Shopify REST to GraphQL Migration: A Plan for Custom Apps
The REST Admin API has been legacy since October 2024. What changes when a custom app or integration moves to GraphQL, a step-by-step plan, and the traps to avoid.
In this guide
Many custom Shopify apps and integrations were written years ago against the REST Admin API. They still pull orders and push stock counts every day, and nobody has touched the code since its developer moved on.
That code is running on borrowed time. Shopify calls the REST Admin API legacy, newer features are GraphQL-only, and parts of REST are already deprecated. This guide explains what actually changes when you move a custom app to the GraphQL Admin API, gives you a migration plan you can follow one resource at a time, and lists the traps that catch most teams.
Where REST stands today
Shopify’s REST Admin API reference opens with two statements:
- “The REST Admin API is a legacy API as of October 1, 2024.”
- “Starting April 1, 2025, all new public apps must be built exclusively with the GraphQL Admin API.”
The same page warns that some newer platform features may only be available in GraphQL. And some REST resources are already on their way out: the REST Product resource says that listing, creating, updating and deleting products is deprecated as of REST API 2024-04, and points to the new product model in GraphQL.
A custom app built for one store is not a public app, so the April 2025 rule does not force it to switch overnight. But a legacy API gets no new features, and every deprecated endpoint it still calls is a future outage. Planning the move now is cheaper than doing it in a hurry when a call stops working.
What actually changes
Authentication is the part that does not change. Shopify’s guide to updating API calls in your app says you don’t need to update your authentication or session handling. The same access token goes in the same X-Shopify-Access-Token header. Almost everything else does change.
| Area | REST Admin API | GraphQL Admin API |
|---|---|---|
| Endpoint | One URL per resource, GET/POST/PUT/DELETE |
One POST endpoint: /admin/api/{version}/graphql.json |
| IDs | Numbers, such as 123 |
Global IDs, such as gid://shopify/Product/123 |
| Pagination | Link headers |
Cursors with first/after and pageInfo |
| Rate limits | Requests in a bucket | Calculated query cost in points |
| Errors | HTTP 4xx/5xx status codes | Often HTTP 200 with an errors array or userErrors |
| Big reads | Page through everything | Bulk operations that return a file |
| Webhooks | REST webhook subscriptions | shopify.app.toml or webhookSubscriptionCreate |
IDs become global IDs
In GraphQL every object has a global ID. A product with the ID 123 becomes gid://shopify/Product/123.
Two fields help you cross over. Most REST resources include an admin_graphql_api_id property with the global ID of the matching GraphQL object, and most GraphQL objects have a legacyResourceId field that gives the plain REST ID back. If your database stores REST IDs, you do not have to rewrite it on day one: read legacyResourceId when you need to match old records.
Pagination moves to cursors
REST pages through results with Link headers. GraphQL uses cursor-based pagination: you ask for first N items, then pass the previous page’s endCursor as after, and stop when hasNextPage is false. A page holds at most 250 items.
query ($cursor: String) {
orders(first: 100, after: $cursor, query: "updated_at:>2026-10-01") {
nodes {
id
name
updatedAt
}
pageInfo {
hasNextPage
endCursor
}
}
}
Shopify’s rate limits overview adds a ceiling worth knowing: paginated lists cap at 25,000 objects, including count queries. For more than that, use a bulk operation.
Rate limits move from requests to cost
The REST Admin API rate limit is a bucket of 40 requests per app per store that empties at 2 per second, or 400 and 20 per second on Shopify Plus. You see it in the X-Shopify-Shop-Api-Call-Limit header and get a 429 with Retry-After when you go over.
The GraphQL Admin API rate limit counts points instead:
| Plan | Points restored per second |
|---|---|
| Standard | 100 |
| Advanced Shopify | 200 |
| Shopify Plus | 1,000 |
| Shopify for enterprise (Commerce Components) | 2,000 |
Each object you request costs 1 point, scalar fields cost 0, a mutation costs 10, and a single query can cost at most 1,000 points. Every response includes extensions.cost with the requested cost, the actual cost and a throttleStatus. Your client should read that block and slow down before it hits a THROTTLED error, rather than waiting for a failure like it did with 429s in REST.
Errors hide inside a 200
This one breaks more migrations than any other. The GraphQL Admin API reference notes that many errors that would be 4xx or 5xx in REST come back as HTTP 200 with an errors object. Mutation problems, such as an invalid field value, appear in userErrors, and only if you ask for it:
mutation ($input: ProductUpdateInput!) {
productUpdate(product: $input) {
product { id }
userErrors { field message }
}
}
Code that checks only the HTTP status will report success for writes that did nothing. Check errors and userErrors on every call.
Large reads become bulk operations
Instead of paging through a year of orders, you can start a bulk query with bulkOperationRunQuery. Shopify runs it in the background and gives you a JSONL file to download; the download URL expires after a week. The query itself is not subject to rate limits, though starting it and checking its status are. From API version 2026-01, an app can run up to five bulk queries per shop at the same time. To know when it is finished, subscribe to the bulk_operations/finish webhook, and poll as a fallback because webhook delivery is not guaranteed.
Webhooks get a new home
Shopify now recommends app-specific webhook subscriptions declared in shopify.app.toml, which apply to every store that installs the app. Shop-specific subscriptions are created with the GraphQL webhookSubscriptionCreate mutation. The api_version under [webhooks] sets which version serializes the payloads, and each HTTPS delivery carries the version in the X-Shopify-API-Version header. When you move webhook subscriptions, check that the payload version matches what your handler expects.
A step-by-step migration plan
1. List every REST call the app makes
Search the codebase for /admin/api/ paths, REST client calls and webhook registrations. Then check what Shopify sees, because old code paths can surprise you:
- The Deprecated API calls endpoint lists the deprecated calls a custom app made in the past 30 days, with the endpoint, last call time and migration deadline. Shopify documents it as available only for custom apps.
- For apps in the Dev Dashboard, the app’s Logs show REST and GraphQL requests, and a Deprecated API calls alert flags calls to APIs scheduled for removal.
Write the list in a table: endpoint, what it is used for, how often it runs, and whether it reads or writes.
2. Order the list by risk
Move deprecated endpoints first, starting with products and variants if you still use them. Then writes, because a silent write failure costs more than a slow read. Leave low-volume reads for last.
3. Map each call to a query or mutation
Most REST requests have a GraphQL equivalent; Shopify’s migration guide walks through mapping endpoints, access scopes, IDs and pagination. Use the GraphiQL explorer to build each query and confirm its cost before you write code. Look for chances to replace several REST calls with one mutation; the product model, for example, has bulk variant mutations and productSet for syncing a full product.
4. Check access scopes
GraphQL lists the required scopes on each type’s reference page. If a query needs a scope the app does not have, add it in a new app version and approve it on the store before you deploy the code.
5. Build a thin API layer
Put all Shopify calls behind one small module that adds the version, reads extensions.cost, retries throttled calls with a backoff, and raises an error on errors or userErrors. Every migrated call then gets the same safety for free.
6. Run old and new side by side
For reads, run the GraphQL version next to the REST one on a development store or a copy of your data and compare the output field by field. For writes, test against a development store first. Pay attention to money fields, dates and anything that used to be a nested REST object.
7. Switch one resource at a time, then remove REST
Deploy the GraphQL path for one resource, watch logs and totals for a few days, then move to the next. When the list is empty, delete the REST client so nobody adds a new call by accident.
Common traps
- Treating HTTP 200 as success. Still the most common bug.
- Ignoring query cost. A query that asks for 250 orders with 250 line items each can blow past the 1,000-point ceiling. Ask for fewer items per page or use a bulk operation.
- Building IDs by string concatenation in many places. Do it in one helper, or use
admin_graphql_api_idfrom data you already have. - Forgetting webhooks. The API calls move to GraphQL, but the webhook handler still expects the old payload version.
- Pinning an old version. Migrating is a good moment to move to a current stable version. Our API version upgrade checklist covers that routine.
- Missing the reconciliation job. If the migrated app syncs data, keep the scheduled catch-up job described in integrations that don’t break.
After the migration
A migration is a good time to look at the rest of the app. Check whether it was created in the Shopify admin before 2026, in which case our guide to custom apps in the Dev Dashboard explains what changes. Then put the app on a regular routine, as described in maintaining a custom Shopify app, so the next platform change is a planned task instead of an incident. If the app connects Shopify to a warehouse or ERP, Shopify ERP and 3PL integration covers the extra checks for that kind of sync.
Having it done for you
If nobody on your team knows the old code, or the app runs something you cannot afford to break, the migration can be handed over. Our custom solutions page describes the Shopify work we take on: the same team that builds and runs Storefleet Store Ops. Before you talk to anyone, brief a developer shows what to write down.
FAQ
Do custom apps have to stop using the REST Admin API?
New public apps must use GraphQL only since April 1, 2025. For custom apps, REST is legacy rather than switched off, but deprecated resources such as REST product writes are already on the way out and new features may be GraphQL-only. Plan the move before a call you depend on is removed.
How do I find which REST calls my custom app still makes?
Search the code for REST paths, then call the Deprecated API calls endpoint, which lists a custom app’s deprecated calls from the past 30 days. Apps in the Dev Dashboard also show requests in their Logs and a Deprecated API calls alert.
Do I need new access tokens to use GraphQL?
No. Shopify’s migration guide says authentication and session handling do not change. You may need extra access scopes if a GraphQL query reads data the app could not read before.
Why does my GraphQL mutation return 200 but change nothing?
GraphQL reports many failures inside a 200 response. Request userErrors on every mutation and check both errors and userErrors before treating a call as successful.
How do I export large amounts of data with GraphQL?
Use a bulk query. Shopify runs it in the background and returns a JSONL file, and the query itself is not subject to the normal rate limits.