If your ERP sync, warehouse connector or in-house script still talks to Shopify's REST Admin API, it's running on a legacy API. Shopify made REST legacy on October 1, 2024. Since April 1, 2025, new public apps must be built only on the GraphQL Admin API. The REST endpoints for products and variants were deprecated in API version 2024-04, and public apps had to move to the GraphQL product APIs by February 1, 2025.
As of this writing, Shopify hasn't published a shutdown date for the remaining REST endpoints used by existing apps. That's not a reason to wait. New platform features ship in GraphQL, and the product model already works around REST's limits. This guide covers what actually changes when you move an integration, and a migration plan that doesn't break production.
Why move now, even without a deadline
- Products and variants. REST product endpoints are deprecated, and custom apps need the GraphQL product APIs to handle products with more than 100 variants.
- New features land in GraphQL. Shopify focuses new development on the GraphQL Admin API, and many newer platform features have no REST equivalent.
- Fewer round trips. One GraphQL query can fetch a product with its variants, inventory and metafields, which took several REST calls.
- Bulk exports. Large catalog or order exports run as one asynchronous bulk operation instead of thousands of paginated requests.
- Fewer surprises. Deprecated REST calls tend to surface as late-night failures after an API version change. Moving on your own schedule is cheaper.
What changes when you switch
- One endpoint. Every request is a POST to
https://{shop}.myshopify.com/admin/api/2026-10/graphql.jsonwith a query and variables. Keep pinning a stable API version and upgrade on a schedule. - IDs become global IDs. GraphQL uses IDs like
gid://shopify/Product/10079785100. Many objects also exposelegacyResourceId, the numeric ID your ERP already stores. Keep a mapping, and don't split strings in several places. - Cursor pagination. Use
firstorlastwithafterorbefore, and readpageInfo { hasNextPage endCursor }. Array inputs are capped at 250 items on every Shopify API, and paginating a list stops at 25,000 objects, so large exports belong in bulk operations. - Rate limits by cost, not request count. Each query has a calculated cost; a single query can't exceed 1,000 points. Every response includes
extensions.costwithrequestedQueryCost,actualQueryCostand athrottleStatus:maximumAvailable,currentlyAvailableandrestoreRate. Throttle from those numbers rather than from a fixed sleep. - Errors come back with HTTP 200. A mutation that fails validation still returns 200, with the problem listed in
userErrors. Checkerrors(malformed queries, throttling) anduserErrorson every call.
A cost-aware paginated fetch
async function shopifyGraphQL(shop, token, query, variables = {}) {
const res = await fetch(`https://${shop}/admin/api/2026-10/graphql.json`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Shopify-Access-Token": token },
body: JSON.stringify({ query, variables }),
});
const body = await res.json();
if (body.errors) throw new Error(JSON.stringify(body.errors));
// Wait if the bucket is nearly empty, based on Shopify's own numbers
const t = body.extensions?.cost?.throttleStatus;
if (t && t.currentlyAvailable < 200) {
const wait = ((200 - t.currentlyAvailable) / t.restoreRate) * 1000;
await new Promise((r) => setTimeout(r, wait));
}
return body.data;
}
const PRODUCTS = `query Products($cursor: String) {
products(first: 100, after: $cursor) {
nodes { id legacyResourceId title variants(first: 100) { nodes { id sku price } } }
pageInfo { hasNextPage endCursor }
}
}`;
async function allProducts(shop, token) {
const out = [];
let cursor = null;
do {
const { products } = await shopifyGraphQL(shop, token, PRODUCTS, { cursor });
out.push(...products.nodes);
cursor = products.pageInfo.hasNextPage ? products.pageInfo.endCursor : null;
} while (cursor);
return out;
}
Keep page sizes moderate, because nested connections multiply the query cost. For full-catalog syncs, use a bulk operation instead of looping.
Always check userErrors on mutations
const UPDATE_PRICE = `mutation UpdatePrice($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants { id price }
userErrors { field message }
}
}`;
const data = await shopifyGraphQL(shop, token, UPDATE_PRICE, {
productId: "gid://shopify/Product/10079785100",
variants: [{ id: "gid://shopify/ProductVariant/43729076", price: "24.99" }],
});
const { userErrors } = data.productVariantsBulkUpdate;
if (userErrors.length) throw new Error(userErrors.map((e) => `${e.field}: ${e.message}`).join("; "));
Large exports: bulk operations
For "every product", "every order this year" or "all inventory levels", start a bulkOperationRunQuery. Shopify runs the query asynchronously and gives you a JSONL file, one JSON object per line. Subscribe to the bulk_operations/finish webhook, or poll the operation's status. From API version 2026-01, an app can run up to five bulk query operations per shop at the same time; earlier versions allowed one. A bulk query must finish within 10 days.
What changed in API version 2026-10
Shopify released API version 2026-10 on October 1, 2026. Three of its changes matter for integrations that are moving to GraphQL:
ProductVariant.barcodeis deprecated. Variants now have abarcodesconnection that holds up to 20 typed barcodes, and the old field returns only the first one. ERP and warehouse syncs should read and writebarcodes.- Invalid metafield filters now return an error. If a query filters by a metafield that isn't valid for filtering, the GraphQL Admin API now fails the query instead of silently ignoring that predicate. Check the filter definitions your searches rely on.
- Updating an order's shipping address recalculates tax. After an address change, re-read
taxLines,totalTaxSetand the order totals, and handle any payment or refund difference.
The full list is in Shopify's 2026-10 release notes. We also track version and platform deadlines on our Shopify deprecation tracker.
A migration plan that doesn't break production
- Inventory every REST call. Search the codebase for
/admin/api/and.jsonendpoints, and log the paths your integration calls in production for a week. Custom apps can also list deprecated calls from the last 30 days through the Deprecated API calls endpoint, and Shopify reports deprecated usage in theX-Shopify-API-Deprecated-Reasonheader and your app's API health report. - Migrate by resource, not all at once. Products and variants go first because they're already deprecated, then orders, inventory and customers.
- Wrap the client once. One module handles auth, the version, cost-based throttling, retries and
userErrors, so no call site talks to the API directly. - Add an ID translation layer. Map
legacyResourceIdvalues to GIDs and store both during the transition. - Shadow-run. For read paths, call REST and GraphQL side by side and compare the results before switching.
- Switch writes last, one resource at a time, with alerts on
userErrors. - Pin and schedule versions. Put the next API version upgrade on the calendar and check the changelog before each quarter's release.
If your integration still runs on an admin-created legacy custom app, read our guide to custom vs public apps first, because the app type decides how it gets an access token.
If your integration is a public app, remember the separate deadline for expiring offline access tokens on January 1, 2027 (our migration guide). It's efficient to do both in the same release.
Common mistakes
- Treating HTTP 200 as success. Check
userErrorson every mutation. - Paginating huge datasets. Use bulk operations for full exports.
- Assuming numeric IDs. GraphQL returns GIDs, so map them explicitly.
- Fixed sleeps between calls. Throttle using
throttleStatus, which is both faster and safer. - Waiting for a sunset date. The product endpoints were already deprecated, and new features won't come to REST.
If your integration also listens to webhooks, check it against our guide to Shopify webhooks you can trust: verification, duplicates, retries and reconciliation.
Connecting an ERP? We compare the options for NetSuite and Business Central separately.
Want it migrated for you?
We move ERP, WMS and custom integrations from REST to the GraphQL Admin API. That includes an audit of your current calls, a shared client with cost-based throttling, ID mapping, shadow runs and a staged cutover. Tell us about your integration and we'll reply with a scoped plan. For app builds and other platform work, see our Shopify development services.
Sources
- Shopify changelog: Starting April 2025, new public apps submitted to the App Store must use GraphQL
- Shopify: REST Admin API reference (legacy status)
- Shopify: GraphQL Admin API reference
- Shopify: API limits
- Shopify: Bulk query operations
- Shopify: Deprecated API calls endpoint
- Shopify: API health report
- Shopify: API version 2026-10 release notes



