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
OAuth 2.0 / Access Token
1,000-point bucket, 50 pts/sec restore
JSON
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.
https://{shop}.myshopify.com/admin/api/2026-04/graphql.jsonSetting 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.
- 1Go to dev.shopify.com and log in with your Shopify Partner account.
- 2Click 'Create app' and select 'Custom app' for single-store use.
- 3Under 'Configuration', find 'Admin API access scopes' and enable `write_inventory` and `read_inventory`.
- 4Click 'Save', then go to 'API credentials' and click 'Install app' to generate the access token.
- 5Copy the Admin API access token — it starts with `shpat_`. Store it in an environment variable immediately; it is shown only once.
- 6Note your shop domain: `{your-store}.myshopify.com` — this is used in every API URL.
- 7To verify the token works, run a test query against the GraphQL endpoint before building the full automation.
1import os2import requests34SHOP_DOMAIN = os.environ['SHOPIFY_SHOP_DOMAIN'] # e.g., mystore.myshopify.com5ACCESS_TOKEN = os.environ['SHOPIFY_ACCESS_TOKEN']67GRAPHQL_URL = f'https://{SHOP_DOMAIN}/admin/api/2026-04/graphql.json'89HEADERS = {10 'X-Shopify-Access-Token': ACCESS_TOKEN,11 'Content-Type': 'application/json',12}1314# Test query to verify authentication15test_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
/admin/api/2026-04/graphql.jsonGraphQL endpoint for the `inventoryAdjustQuantities` mutation. Adjusts inventory levels by delta amounts across one or more locations in a single request.
| Parameter | Type | Required | Description |
|---|---|---|---|
inventoryItemId | string | required | Global ID of the inventory item (gid://shopify/InventoryItem/{id}) |
locationId | string | required | Global ID of the location (gid://shopify/Location/{id}) |
delta | number | required | Positive or negative integer quantity change |
reason | string | required | Reason code: 'correction', 'cycle_count_available', 'damaged', 'movement_created', 'received', 'reservation_created', 'shrinkage' |
Request
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
1{"data": {"inventoryAdjustQuantities": {"userErrors": [], "inventoryAdjustmentGroup": {"createdAt": "2026-04-15T10:30:00Z", "reason": "correction", "changes": [{"name": "available", "delta": -5, "quantityAfterChange": 95}]}}}}/admin/api/2026-04/graphql.jsonGraphQL endpoint for the `inventorySetQuantities` mutation. Sets absolute inventory quantities, overwriting current values. Use when syncing from a source of truth.
| Parameter | Type | Required | Description |
|---|---|---|---|
quantities | array | required | Array of {inventoryItemId, locationId, quantity} objects |
ignoreCompareQuantity | boolean | optional | Set to true to overwrite without optimistic locking check |
Request
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
1{"data": {"inventorySetQuantities": {"userErrors": [], "inventoryAdjustmentGroup": {"reason": "correction", "changes": [{"name": "available", "quantityAfterChange": 100}]}}}}/admin/api/2026-04/graphql.jsonGraphQL query to fetch inventory levels for a location, returning inventoryItemId and current available quantity for mapping to your SKUs.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Location global ID |
first | number | optional | Number of inventory levels to return per page (max 250) |
Request
1{"query": "{ location(id: \"gid://shopify/Location/87654321\") { inventoryLevels(first: 50) { edges { node { id available item { id sku } } } pageInfo { hasNextPage endCursor } } } }"}Response
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}}}}}/admin/api/2026-04/graphql.jsonGraphQL mutation to create a webhook subscription for `inventory_levels/update` events, enabling real-time notification when inventory changes.
| Parameter | Type | Required | Description |
|---|---|---|---|
topic | string | required | Webhook topic: INVENTORY_LEVELS_UPDATE |
callbackUrl | string | required | HTTPS URL to receive webhook payloads |
Request
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
1{"data": {"webhookSubscriptionCreate": {"userErrors": [], "webhookSubscription": {"id": "gid://shopify/WebhookSubscription/99887766"}}}}Step-by-step automation
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.
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.
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.
1# Supplier feeds are typically CSV — parse them before calling the API2# This step is preparatory; the actual API call is in Step 33echo '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.
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.
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.
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.
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.
1import os2import csv3import time4import logging5import requests67logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')89SHOP_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 = 501415URL = f'https://{SHOP_DOMAIN}/admin/api/2026-04/graphql.json'16HEADERS = {'X-Shopify-Access-Token': ACCESS_TOKEN, 'Content-Type': 'application/json'}1718def 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()2223def fetch_inventory_map():24 """Returns {sku: {item_id, available}}"""25 inventory = {}26 cursor = None27 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 break36 cursor = data['pageInfo']['endCursor']37 return inventory3839def 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 feed4546def 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 result5859def 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')6364 logging.info(f'Parsing supplier feed from {SUPPLIER_FEED_PATH}...')65 feed = parse_feed(SUPPLIER_FEED_PATH)6667 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 continue72 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})7576 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}')8182 logging.info('Inventory sync complete')8384if __name__ == '__main__':85 main()Error handling
ThrottledGraphQL point bucket exhausted (currentlyAvailable = 0). Triggered when mutations are sent faster than the 50 pts/sec restore rate on Standard plans.
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.
Wait for bucket to restore: `sleep((requestedQueryCost - currentlyAvailable) / restoreRate)`. For persistent throttling, reduce batch size and add a 200ms delay between requests.
Variable $input of type InventoryAdjustQuantitiesInput! was provided invalid valueThe inventoryItemId or locationId global ID is malformed, or the inventory item is not tracked at the specified location.
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.
Do not retry without fixing the payload. Log the failed SKU and continue with the rest of the batch.
Unauthorized: access token is invalidAccess token has been revoked (app uninstalled, secret rotated) or token has expired (expiring offline tokens expire after ~60 minutes).
For expiring offline tokens, implement the refresh token flow. For custom app tokens, regenerate from the Dev Dashboard. For public apps, trigger re-authorization.
No retry on 401 — fix authentication first. Implement automatic token refresh with the new expiring token model to avoid future 401s.
Forbidden: missing required scopeThe 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.
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.
No retry — scope changes require re-authorization.
Unprocessable Entity: delta would result in negative inventoryThe adjustment would set available quantity below zero. Shopify prevents going negative unless the product has 'Continue selling when out of stock' enabled.
Check the product variant's `inventoryPolicy` setting. Either enable overselling on the variant, or cap the delta so that `current_qty + delta >= 0`.
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
| Scope | Limit | Window |
|---|---|---|
| Standard plan — GraphQL bucket | 1,000 points | Restores at 50 points/second |
| Plus plan — GraphQL bucket | 10,000 points | Restores at 500 points/second |
| Per mutation cost | ~10 points | Per inventoryAdjustQuantities call regardless of items count |
| Webhook delivery timeout | 5 seconds | Per webhook delivery attempt; 19 failures = subscription deleted |
1import time23def 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 ** attempt8 print(f'Rate limited, waiting {wait}s')9 time.sleep(wait)10 continue11 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_rate18 time.sleep(sleep_time)19 return result20 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
intermediateDaily or hourly import of supplier CSV/JSON feeds to keep Shopify inventory aligned with warehouse stock levels.
Multi-Location Redistribution
advancedAutomatically rebalance stock across multiple Shopify locations based on sales velocity data from each location.
Multichannel Inventory Sync
advancedSync Shopify inventory with Amazon FBM, Etsy, and other sales channels to prevent overselling across platforms.
Low-Stock Alerts
beginnerMonitor 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/monthZapier's Shopify integration can update inventory levels when triggered by Google Sheets changes or webhook events, no code required.
- + No code required
- + 500+ integrations for supplier systems
- + Easy to set up in minutes
- - 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/monthMake supports bulk Shopify inventory operations via HTTP modules and handles complex CSV parsing with data transformations.
- + Supports looping over CSV rows
- + More affordable than Zapier at scale
- + Visual flow builder
- - Learning curve for complex scenarios
- - No native GraphQL support
- - Rate limiting not auto-managed
n8n
Self-hosted free; Cloud Starter €20/monthn8n's Shopify node and HTTP Request node support GraphQL mutations for inventory updates with self-hosted deployment.
- + Self-hosted option (free)
- + GraphQL support via HTTP node
- + Full control over batching logic
- - 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.
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.
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.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation