Skip to main content
RapidDev - Software Development Agency
API AutomationsShopifyBearer Token

How to Automate Shopify Inventory Updates using the API

Automate Shopify inventory updates using the GraphQL Admin API's `inventoryAdjustQuantities` mutation. Send adjustments per SKU and location with a POST to `https://{shop}.myshopify.com/admin/api/2026-04/graphql.json`. The key rate limit: 1,000-point GraphQL bucket on Standard plans with 50 pts/sec restore — each mutation costs ~10 points, giving you a sustained rate of ~5 inventory updates per second.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate7 min read30-60 minutesShopifyMay 2026RapidDev Engineering Team
TL;DR

Automate Shopify inventory updates using the GraphQL Admin API's `inventoryAdjustQuantities` mutation. Send adjustments per SKU and location with a POST to `https://{shop}.myshopify.com/admin/api/2026-04/graphql.json`. The key rate limit: 1,000-point GraphQL bucket on Standard plans with 50 pts/sec restore — each mutation costs ~10 points, giving you a sustained rate of ~5 inventory updates per second.

API Quick Reference

Auth

OAuth 2.0 / Access Token

Rate limit

1,000-point bucket, 50 pts/sec restore

Format

JSON

SDK

Available

Understanding the Shopify GraphQL Admin API for Inventory

Shopify's Admin API handles all backend store operations — products, orders, customers, and inventory. Since April 1, 2025, all new public apps must use the GraphQL Admin API exclusively; REST inventory endpoints still function for custom apps but are in maintenance mode. The current stable API version is 2026-04.

Inventory in Shopify is location-aware. Every inventory item (tied to a product variant) has separate quantity levels per location. The `inventoryAdjustQuantities` mutation adjusts stock by delta (e.g., +5 or -3), while `inventorySetQuantities` overwrites to an absolute value. For supplier feed integrations, `inventoryAdjustQuantities` is safer because it handles concurrent updates gracefully.

For stores with 1,000+ SKUs or multiple locations, the Bulk Operations API (`bulkOperationRunMutation`) bypasses point-based rate limits entirely, submitting a JSONL file of mutations and returning results asynchronously. This is the recommended approach for full catalog syncs. See the official docs at https://shopify.dev/docs/api/admin-graphql.

Base URLhttps://{shop}.myshopify.com/admin/api/2026-04/graphql.json

Setting Up Shopify API Authentication

Shopify Admin API uses OAuth 2.0 for public/distributed apps and a simpler direct access token for custom apps. For inventory automation on a single store, a custom app access token is the fastest path. As of January 1, 2026, new custom apps must be created via the Shopify Dev Dashboard (dev.shopify.com) or CLI — you can no longer create them from the Shopify Admin.

  1. 1Go to dev.shopify.com and log in with your Shopify Partner account.
  2. 2Click 'Create app' and select 'Custom app' for single-store use.
  3. 3Under 'Configuration', find 'Admin API access scopes' and enable `write_inventory` and `read_inventory`.
  4. 4Click 'Save', then go to 'API credentials' and click 'Install app' to generate the access token.
  5. 5Copy the Admin API access token — it starts with `shpat_`. Store it in an environment variable immediately; it is shown only once.
  6. 6Note your shop domain: `{your-store}.myshopify.com` — this is used in every API URL.
  7. 7To verify the token works, run a test query against the GraphQL endpoint before building the full automation.
auth.py
1import os
2import requests
3
4SHOP_DOMAIN = os.environ['SHOPIFY_SHOP_DOMAIN'] # e.g., mystore.myshopify.com
5ACCESS_TOKEN = os.environ['SHOPIFY_ACCESS_TOKEN']
6
7GRAPHQL_URL = f'https://{SHOP_DOMAIN}/admin/api/2026-04/graphql.json'
8
9HEADERS = {
10 'X-Shopify-Access-Token': ACCESS_TOKEN,
11 'Content-Type': 'application/json',
12}
13
14# Test query to verify authentication
15test_query = '{shop { name id }}'
16response = requests.post(GRAPHQL_URL, json={'query': test_query}, headers=HEADERS)
17print(response.json())

Security notes

  • Store the access token in environment variables — never hardcode it in source files.
  • The access token is shown only once on creation — store it in a secrets manager immediately.
  • Rotate the token if you suspect it has been exposed; generate a new one from the Dev Dashboard.
  • For public apps using OAuth, validate the HMAC on every webhook using base64(HMAC-SHA256), NOT hex — this is the most common authentication bug.
  • Limit app scopes to only what is needed: `read_inventory` and `write_inventory` for this automation.
  • Use HTTPS for all API calls — never send the token over plain HTTP.

Key endpoints

POST/admin/api/2026-04/graphql.json

GraphQL endpoint for the `inventoryAdjustQuantities` mutation. Adjusts inventory levels by delta amounts across one or more locations in a single request.

ParameterTypeRequiredDescription
inventoryItemIdstringrequiredGlobal ID of the inventory item (gid://shopify/InventoryItem/{id})
locationIdstringrequiredGlobal ID of the location (gid://shopify/Location/{id})
deltanumberrequiredPositive or negative integer quantity change
reasonstringrequiredReason code: 'correction', 'cycle_count_available', 'damaged', 'movement_created', 'received', 'reservation_created', 'shrinkage'

Request

json
1{"query": "mutation inventoryAdjustQuantities($input: InventoryAdjustQuantitiesInput!) { inventoryAdjustQuantities(input: $input) { userErrors { field message } inventoryAdjustmentGroup { createdAt reason changes { name delta quantityAfterChange } } } }", "variables": {"input": {"reason": "correction", "name": "available", "changes": [{"inventoryItemId": "gid://shopify/InventoryItem/12345678", "locationId": "gid://shopify/Location/87654321", "delta": -5}]}}}

Response

json
1{"data": {"inventoryAdjustQuantities": {"userErrors": [], "inventoryAdjustmentGroup": {"createdAt": "2026-04-15T10:30:00Z", "reason": "correction", "changes": [{"name": "available", "delta": -5, "quantityAfterChange": 95}]}}}}
POST/admin/api/2026-04/graphql.json

GraphQL endpoint for the `inventorySetQuantities` mutation. Sets absolute inventory quantities, overwriting current values. Use when syncing from a source of truth.

ParameterTypeRequiredDescription
quantitiesarrayrequiredArray of {inventoryItemId, locationId, quantity} objects
ignoreCompareQuantitybooleanoptionalSet to true to overwrite without optimistic locking check

Request

json
1{"query": "mutation inventorySetQuantities($input: InventorySetQuantitiesInput!) { inventorySetQuantities(input: $input) { userErrors { field message } inventoryAdjustmentGroup { reason changes { name quantityAfterChange } } } }", "variables": {"input": {"reason": "correction", "name": "available", "ignoreCompareQuantity": true, "quantities": [{"inventoryItemId": "gid://shopify/InventoryItem/12345678", "locationId": "gid://shopify/Location/87654321", "quantity": 100}]}}}

Response

json
1{"data": {"inventorySetQuantities": {"userErrors": [], "inventoryAdjustmentGroup": {"reason": "correction", "changes": [{"name": "available", "quantityAfterChange": 100}]}}}}
POST/admin/api/2026-04/graphql.json

GraphQL query to fetch inventory levels for a location, returning inventoryItemId and current available quantity for mapping to your SKUs.

ParameterTypeRequiredDescription
idstringrequiredLocation global ID
firstnumberoptionalNumber of inventory levels to return per page (max 250)

Request

json
1{"query": "{ location(id: \"gid://shopify/Location/87654321\") { inventoryLevels(first: 50) { edges { node { id available item { id sku } } } pageInfo { hasNextPage endCursor } } } }"}

Response

json
1{"data": {"location": {"inventoryLevels": {"edges": [{"node": {"id": "gid://shopify/InventoryLevel/87654321?inventory_item_id=12345678", "available": 100, "item": {"id": "gid://shopify/InventoryItem/12345678", "sku": "WIDGET-RED-M"}}}], "pageInfo": {"hasNextPage": false, "endCursor": null}}}}}
POST/admin/api/2026-04/graphql.json

GraphQL mutation to create a webhook subscription for `inventory_levels/update` events, enabling real-time notification when inventory changes.

ParameterTypeRequiredDescription
topicstringrequiredWebhook topic: INVENTORY_LEVELS_UPDATE
callbackUrlstringrequiredHTTPS URL to receive webhook payloads

Request

json
1{"query": "mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) { webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) { userErrors { field message } webhookSubscription { id } } }", "variables": {"topic": "INVENTORY_LEVELS_UPDATE", "webhookSubscription": {"callbackUrl": "https://yourapp.com/webhooks/inventory", "format": "JSON"}}}

Response

json
1{"data": {"webhookSubscriptionCreate": {"userErrors": [], "webhookSubscription": {"id": "gid://shopify/WebhookSubscription/99887766"}}}}

Step-by-step automation

1

Fetch Inventory Item IDs by SKU

Why: Shopify inventory mutations require global IDs (gid://shopify/InventoryItem/...) — you need to map your SKUs to these IDs before you can update quantities.

Query the GraphQL API to get all inventory items with their SKUs and IDs for a given location. Paginate through results using `endCursor` and `hasNextPage`. Build a dict mapping SKU to inventoryItemId for use in subsequent steps.

request.sh
1curl -X POST 'https://mystore.myshopify.com/admin/api/2026-04/graphql.json' \
2 -H 'X-Shopify-Access-Token: shpat_your_token' \
3 -H 'Content-Type: application/json' \
4 -d '{
5 "query": "{ location(id: \\"gid://shopify/Location/87654321\\") { inventoryLevels(first: 250) { edges { node { available item { id sku } } } pageInfo { hasNextPage endCursor } } } }"
6 }'

Pro tip: Cache this SKU-to-ID map and refresh it only when products change — it rarely changes and fetching it on every sync wastes rate-limit budget.

Expected result: A dictionary mapping each SKU (e.g., 'WIDGET-RED-M') to its Shopify InventoryItem global ID.

2

Parse Supplier Feed and Calculate Deltas

Why: The `inventoryAdjustQuantities` mutation adjusts by delta, so you need to compare your supplier's stock levels against Shopify's current quantities to compute the correct change amount.

Load your supplier CSV/JSON feed containing SKU and quantity data. For each SKU, look up the current Shopify quantity (from Step 1's node data) and calculate the delta (supplier_qty - shopify_qty). Batch SKUs with non-zero deltas into groups of ~50 to keep mutation cost under 500 points per request.

request.sh
1# Supplier feeds are typically CSV parse them before calling the API
2# This step is preparatory; the actual API call is in Step 3
3echo 'Parse supplier feed in your language of choice, compute deltas'

Pro tip: Batch changes in groups of 50 or fewer — each item in `changes` costs ~10 points, so 50 items = ~500 points per mutation, leaving buffer for retries.

Expected result: A filtered list of SKUs with non-zero deltas, ready to batch into mutations.

3

Apply Inventory Adjustments via GraphQL Mutation

Why: The `inventoryAdjustQuantities` mutation updates stock levels in Shopify — this is the core API call that prevents overselling.

Send batched `inventoryAdjustQuantities` mutations using the SKU-to-ID map from Step 1 and deltas from Step 2. Check the `userErrors` field in every response — Shopify returns HTTP 200 even for logical errors (unknown inventory item ID, invalid location). Respect the cost budget by checking `throttleStatus.currentlyAvailable` in the response extensions.

request.sh
1curl -X POST 'https://mystore.myshopify.com/admin/api/2026-04/graphql.json' \
2 -H 'X-Shopify-Access-Token: shpat_your_token' \
3 -H 'Content-Type: application/json' \
4 -d '{
5 "query": "mutation inventoryAdjustQuantities($input: InventoryAdjustQuantitiesInput!) { inventoryAdjustQuantities(input: $input) { userErrors { field message } inventoryAdjustmentGroup { reason changes { name delta quantityAfterChange } } } }",
6 "variables": {
7 "input": {
8 "reason": "correction",
9 "name": "available",
10 "changes": [
11 {"inventoryItemId": "gid://shopify/InventoryItem/12345678", "locationId": "gid://shopify/Location/87654321", "delta": -5},
12 {"inventoryItemId": "gid://shopify/InventoryItem/23456789", "locationId": "gid://shopify/Location/87654321", "delta": 10}
13 ]
14 }
15 }
16 }'

Pro tip: Always check `userErrors` — HTTP 200 does not mean the mutation succeeded. A missing inventory item or invalid location ID will return a user error, not a 4xx.

Expected result: Each mutation returns `inventoryAdjustmentGroup` with the changes applied and `quantityAfterChange` for each SKU. `userErrors` is empty on success.

4

Register Webhook for Real-Time Inventory Change Notifications

Why: Webhooks let you react to inventory changes caused by other systems (Shopify admin, other apps) so your sync stays accurate without polling.

Use the `webhookSubscriptionCreate` mutation to subscribe to `INVENTORY_LEVELS_UPDATE`. Shopify will POST to your endpoint within seconds of any inventory change. Return a 2xx response within 5 seconds to avoid counting as a failed delivery — after 19 consecutive failures, Shopify silently deletes the subscription.

request.sh
1curl -X POST 'https://mystore.myshopify.com/admin/api/2026-04/graphql.json' \
2 -H 'X-Shopify-Access-Token: shpat_your_token' \
3 -H 'Content-Type: application/json' \
4 -d '{
5 "query": "mutation { webhookSubscriptionCreate(topic: INVENTORY_LEVELS_UPDATE, webhookSubscription: {callbackUrl: \\"https://yourapp.com/webhooks/inventory\\", format: JSON}) { userErrors { field message } webhookSubscription { id } } }"
6 }'

Pro tip: Monitor your webhook subscription health daily. Shopify silently deletes subscriptions after 19 consecutive failed deliveries (~48 hours) with no notification — your automation stops working silently.

Expected result: Webhook subscription created and confirmed with an ID. Your endpoint receives POST requests with inventory change data within seconds of any update.

Complete working code

This script reads a supplier CSV feed, fetches the current Shopify inventory for your primary location, computes deltas, and applies batched `inventoryAdjustQuantities` mutations while respecting the GraphQL rate limit bucket. It logs all changes and errors.

automate_shopify_inventory.py
1import os
2import csv
3import time
4import logging
5import requests
6
7logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
8
9SHOP_DOMAIN = os.environ['SHOPIFY_SHOP_DOMAIN']
10ACCESS_TOKEN = os.environ['SHOPIFY_ACCESS_TOKEN']
11LOCATION_ID = os.environ['SHOPIFY_LOCATION_ID']
12SUPPLIER_FEED_PATH = os.environ.get('SUPPLIER_FEED_PATH', 'supplier_feed.csv')
13BATCH_SIZE = 50
14
15URL = f'https://{SHOP_DOMAIN}/admin/api/2026-04/graphql.json'
16HEADERS = {'X-Shopify-Access-Token': ACCESS_TOKEN, 'Content-Type': 'application/json'}
17
18def graphql(query, variables=None):
19 resp = requests.post(URL, json={'query': query, 'variables': variables or {}}, headers=HEADERS)
20 resp.raise_for_status()
21 return resp.json()
22
23def fetch_inventory_map():
24 """Returns {sku: {item_id, available}}"""
25 inventory = {}
26 cursor = None
27 while True:
28 after = f', after: "{cursor}"' if cursor else ''
29 q = f'{{ location(id: "{LOCATION_ID}") {{ inventoryLevels(first: 250{after}) {{ edges {{ node {{ available item {{ id sku }} }} }} pageInfo {{ hasNextPage endCursor }} }} }} }}'
30 data = graphql(q)['data']['location']['inventoryLevels']
31 for edge in data['edges']:
32 n = edge['node']
33 inventory[n['item']['sku']] = {'item_id': n['item']['id'], 'available': n['available']}
34 if not data['pageInfo']['hasNextPage']:
35 break
36 cursor = data['pageInfo']['endCursor']
37 return inventory
38
39def parse_feed(path):
40 feed = {}
41 with open(path) as f:
42 for row in csv.DictReader(f):
43 feed[row['SKU']] = int(row['quantity'])
44 return feed
45
46def adjust_batch(changes):
47 mutation = 'mutation inventoryAdjustQuantities($input: InventoryAdjustQuantitiesInput!) { inventoryAdjustQuantities(input: $input) { userErrors { field message } inventoryAdjustmentGroup { changes { name delta quantityAfterChange } } } }'
48 variables = {'input': {'reason': 'correction', 'name': 'available', 'changes': changes}}
49 result = graphql(mutation, variables)
50 cost = result.get('extensions', {}).get('cost', {}).get('throttleStatus', {})
51 if cost.get('currentlyAvailable', 1000) < 150:
52 logging.info('Throttle low, sleeping 3s')
53 time.sleep(3)
54 errors = result['data']['inventoryAdjustQuantities']['userErrors']
55 if errors:
56 logging.error(f'User errors: {errors}')
57 return result
58
59def main():
60 logging.info('Fetching Shopify inventory map...')
61 inventory_map = fetch_inventory_map()
62 logging.info(f'Found {len(inventory_map)} SKUs in Shopify')
63
64 logging.info(f'Parsing supplier feed from {SUPPLIER_FEED_PATH}...')
65 feed = parse_feed(SUPPLIER_FEED_PATH)
66
67 changes = []
68 for sku, supplier_qty in feed.items():
69 if sku not in inventory_map:
70 logging.warning(f'SKU {sku} not found in Shopify, skipping')
71 continue
72 delta = supplier_qty - inventory_map[sku]['available']
73 if delta != 0:
74 changes.append({'inventoryItemId': inventory_map[sku]['item_id'], 'locationId': LOCATION_ID, 'delta': delta})
75
76 logging.info(f'{len(changes)} SKUs require adjustment')
77 for i in range(0, len(changes), BATCH_SIZE):
78 batch = changes[i:i + BATCH_SIZE]
79 adjust_batch(batch)
80 logging.info(f'Processed batch {i // BATCH_SIZE + 1}')
81
82 logging.info('Inventory sync complete')
83
84if __name__ == '__main__':
85 main()

Error handling

429Throttled
Cause

GraphQL point bucket exhausted (currentlyAvailable = 0). Triggered when mutations are sent faster than the 50 pts/sec restore rate on Standard plans.

Fix

Check `extensions.cost.throttleStatus.currentlyAvailable` in every GraphQL response. When it drops below 100, pause for (100 - currentlyAvailable) / 50 seconds. For REST, honor the `Retry-After` header.

Retry strategy

Wait for bucket to restore: `sleep((requestedQueryCost - currentlyAvailable) / restoreRate)`. For persistent throttling, reduce batch size and add a 200ms delay between requests.

200 with userErrorsVariable $input of type InventoryAdjustQuantitiesInput! was provided invalid value
Cause

The inventoryItemId or locationId global ID is malformed, or the inventory item is not tracked at the specified location.

Fix

Verify that the inventory item is enabled for the location by checking the `inventoryLevel` for that location. Ensure IDs use the `gid://shopify/InventoryItem/` prefix. Re-fetch the SKU map if the catalog has changed recently.

Retry strategy

Do not retry without fixing the payload. Log the failed SKU and continue with the rest of the batch.

401Unauthorized: access token is invalid
Cause

Access token has been revoked (app uninstalled, secret rotated) or token has expired (expiring offline tokens expire after ~60 minutes).

Fix

For expiring offline tokens, implement the refresh token flow. For custom app tokens, regenerate from the Dev Dashboard. For public apps, trigger re-authorization.

Retry strategy

No retry on 401 — fix authentication first. Implement automatic token refresh with the new expiring token model to avoid future 401s.

403Forbidden: missing required scope
Cause

The app's access token does not include the `write_inventory` scope. This happens if the scope was added after the token was generated without reinstalling the app.

Fix

Add `write_inventory` to the app's Admin API access scopes in the Dev Dashboard, then reinstall the app (for custom apps) or trigger re-OAuth (for public apps) to generate a new token with the updated scopes.

Retry strategy

No retry — scope changes require re-authorization.

422Unprocessable Entity: delta would result in negative inventory
Cause

The adjustment would set available quantity below zero. Shopify prevents going negative unless the product has 'Continue selling when out of stock' enabled.

Fix

Check the product variant's `inventoryPolicy` setting. Either enable overselling on the variant, or cap the delta so that `current_qty + delta >= 0`.

Retry strategy

Do not retry — this is a data validation issue. Log the SKU and adjust your supplier feed parsing logic.

Rate Limits for Shopify GraphQL Admin API

ScopeLimitWindow
Standard plan — GraphQL bucket1,000 pointsRestores at 50 points/second
Plus plan — GraphQL bucket10,000 pointsRestores at 500 points/second
Per mutation cost~10 pointsPer inventoryAdjustQuantities call regardless of items count
Webhook delivery timeout5 secondsPer webhook delivery attempt; 19 failures = subscription deleted
retry-handler.ts
1import time
2
3def graphql_with_backoff(url, headers, payload, max_retries=5):
4 for attempt in range(max_retries):
5 resp = requests.post(url, json=payload, headers=headers)
6 if resp.status_code == 429:
7 wait = 2 ** attempt
8 print(f'Rate limited, waiting {wait}s')
9 time.sleep(wait)
10 continue
11 resp.raise_for_status()
12 result = resp.json()
13 cost = result.get('extensions', {}).get('cost', {}).get('throttleStatus', {})
14 available = cost.get('currentlyAvailable', 1000)
15 if available < 100:
16 restore_rate = cost.get('restoreRate', 50)
17 sleep_time = (200 - available) / restore_rate
18 time.sleep(sleep_time)
19 return result
20 raise Exception('Max retries exceeded')
  • Monitor `extensions.cost.throttleStatus.currentlyAvailable` on every GraphQL response and proactively throttle when below 150 points.
  • Use `bulkOperationRunMutation` for full catalog syncs (1,000+ SKUs) — bulk operations bypass the point bucket entirely.
  • Batch up to 50 items per `inventoryAdjustQuantities` mutation to reduce total API call count.
  • Cache the SKU-to-InventoryItemID mapping — it changes only when variants are added/removed, not on every sync.
  • On Standard plans, sustained throughput is ~5 inventory updates/second — plan nightly sync windows for large catalogs.

Security checklist

  • Store SHOPIFY_ACCESS_TOKEN in environment variables or a secrets manager — never in source code or version control.
  • Validate webhook HMAC using base64(HMAC-SHA256(rawBody, appSecret)) with constant-time comparison — NOT hex digest.
  • Verify webhook payloads against the raw request body before JSON parsing — HMAC is computed over raw bytes.
  • Limit app scopes to the minimum required: read_inventory and write_inventory only for this automation.
  • Monitor webhook subscription health daily — Shopify silently deletes subscriptions after 19 consecutive failures.
  • Use HTTPS for all webhook callback URLs — Shopify rejects HTTP endpoints.
  • Rotate access tokens if any exposure is suspected — new tokens are generated instantly from the Dev Dashboard.
  • Log all inventory mutations with timestamps and deltas for audit trail and debugging.

Automation use cases

Supplier Feed Sync

intermediate

Daily or hourly import of supplier CSV/JSON feeds to keep Shopify inventory aligned with warehouse stock levels.

Multi-Location Redistribution

advanced

Automatically rebalance stock across multiple Shopify locations based on sales velocity data from each location.

Multichannel Inventory Sync

advanced

Sync Shopify inventory with Amazon FBM, Etsy, and other sales channels to prevent overselling across platforms.

Low-Stock Alerts

beginner

Monitor inventory levels via the `inventory_levels/update` webhook and send Slack/email alerts when any SKU drops below a threshold.

No-code alternatives

Don't want to write code? These platforms can automate the same workflows visually.

Zapier

Free tier available; Starter $19.99/month

Zapier's Shopify integration can update inventory levels when triggered by Google Sheets changes or webhook events, no code required.

Pros
  • + No code required
  • + 500+ integrations for supplier systems
  • + Easy to set up in minutes
Cons
  • - Limited to 1 SKU per Zap run (no bulk)
  • - Expensive at scale ($49+/month for multi-step)
  • - No GraphQL access — uses legacy REST

Make (formerly Integromat)

Free tier (1,000 ops/month); Core $9/month

Make supports bulk Shopify inventory operations via HTTP modules and handles complex CSV parsing with data transformations.

Pros
  • + Supports looping over CSV rows
  • + More affordable than Zapier at scale
  • + Visual flow builder
Cons
  • - Learning curve for complex scenarios
  • - No native GraphQL support
  • - Rate limiting not auto-managed

n8n

Self-hosted free; Cloud Starter €20/month

n8n's Shopify node and HTTP Request node support GraphQL mutations for inventory updates with self-hosted deployment.

Pros
  • + Self-hosted option (free)
  • + GraphQL support via HTTP node
  • + Full control over batching logic
Cons
  • - Requires technical setup
  • - Self-hosting means managing infrastructure
  • - Less documentation than Zapier

Best practices

  • Use `inventoryAdjustQuantities` (delta) over `inventorySetQuantities` (absolute) for concurrent-safe updates when multiple systems touch inventory.
  • Always check `userErrors` in GraphQL responses — HTTP 200 does not guarantee a successful mutation.
  • Implement idempotency by tracking which supplier feed batches have been applied — use a hash of the feed file as a deduplication key.
  • For stores with 1,000+ SKUs, prefer `bulkOperationRunMutation` which submits a JSONL file and bypasses the point bucket entirely.
  • Include the `reason` field on every inventory adjustment (e.g., 'correction', 'received') — Shopify logs this for store owners and it aids debugging.
  • Test with a staging store before running against production — Shopify's sandbox environment uses canned data but the GraphQL schema is identical.
  • Monitor `X-Shopify-API-Version` response header to catch silent version fall-forward if your pinned version gets retired.
  • Set up the `inventory_levels/update` webhook to detect manual inventory changes in the Shopify admin that might conflict with automated syncs.

Ask AI to help

Copy one of these prompts to get a personalized, working implementation.

ChatGPT / Claude Prompt

I'm building a Shopify inventory sync automation using the GraphQL Admin API (2026-04). I'm using the `inventoryAdjustQuantities` mutation with batches of 50 SKUs. My current issue is: [describe your error or challenge]. Here's my current code: [paste code]. The response I'm getting is: [paste response including extensions.cost]. Help me fix this and ensure I'm handling the throttle status correctly.

Lovable / V0 Prompt

Build a Shopify inventory management dashboard that shows current stock levels by SKU and location, displays a sync history log with timestamps and delta changes, and has a button to trigger a manual CSV upload for bulk inventory adjustment. Connect to Supabase for storing sync logs. Use shadcn/ui components and Tailwind CSS. The dashboard should show real-time throttle budget remaining.

Frequently asked questions

Does Shopify still support REST inventory endpoints?

Yes, REST inventory endpoints (inventory_levels/adjust.json, inventory_levels/set.json) still function for custom apps. However, all new public apps must use GraphQL since April 1, 2025. Shopify has declared REST as 'legacy' — it receives no new features and may be deprecated in a future version. For any new integration, use `inventoryAdjustQuantities` or `inventorySetQuantities` GraphQL mutations.

What is the difference between inventoryAdjustQuantities and inventorySetQuantities?

`inventoryAdjustQuantities` changes stock by a delta (e.g., delta: -5 removes 5 units). It's safer for concurrent systems because two adjustments won't overwrite each other. `inventorySetQuantities` overwrites to an absolute value (e.g., quantity: 100 sets to exactly 100). Use `inventorySetQuantities` when you have a full snapshot from a warehouse system; use `inventoryAdjustQuantities` when processing incremental changes.

What happens when I hit the GraphQL rate limit?

Shopify returns HTTP 429 with a `Retry-After` header indicating seconds to wait. For GraphQL specifically, you can monitor the `extensions.cost.throttleStatus.currentlyAvailable` field on every 200 response — this tells you remaining bucket points before you'd be throttled. On Standard plans: 1,000-point bucket, 50 pts/sec restore. A mutation costs ~10 points. Implement backoff logic that sleeps when `currentlyAvailable < 100`.

How do I handle inventory updates for multi-location stores?

Each `inventoryAdjustQuantities` change object requires both `inventoryItemId` AND `locationId`. For multi-location sync, you need to send separate changes per location in the same mutation. First, fetch your location IDs with `{ locations(first: 10) { edges { node { id name } } } }`. Then build your changes array with one entry per SKU-location combination.

What is a Bulk Operation and when should I use it for inventory?

Bulk operations (`bulkOperationRunMutation`) let you submit a JSONL file containing thousands of mutations without consuming GraphQL point budget. The operation runs asynchronously — you poll its status or subscribe to the `bulk_operations/finish` webhook. Use bulk operations when syncing more than ~500 SKUs at once, or for nightly full-catalog reconciliations. For real-time adjustments (e.g., after a sale), stick with regular mutations.

Can I create a custom app from the Shopify Admin anymore?

No. As of January 1, 2026, new custom apps can no longer be created from the Shopify Admin interface. You must create them via the Shopify Dev Dashboard (dev.shopify.com) or the Shopify CLI. Existing custom apps created before this date continue to function normally.

Can RapidDev help build a custom Shopify inventory integration?

Yes. RapidDev has built 600+ apps including Shopify integrations with multi-location inventory sync, supplier feed pipelines, and 3PL connections. We handle the GraphQL API, webhook infrastructure, and rate limit management. Book a free consultation at rapidevelopers.com.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Skip the coding — we'll build it for you

Our experts have built 600+ API automations. From prototype to production in days, not weeks.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.