Dieser Artikel ist derzeit nur auf Englisch verfügbar. Die Übersetzung folgt bald.
Shopify GraphQL Admin API 2026: Complete Developer Guide
Shopify deprecated unversioned REST endpoints for new public apps in October 2024 and is steadily moving the entire Admin API surface to GraphQL. By API version 2026-01, GraphQL is no longer optional for serious app developers — products, orders, customers, fulfillment, metafields, and bulk operations all expose richer functionality there than the legacy REST routes. This guide walks through authentication, the calculated query cost model, mutations, error handling, and production patterns we use across our Shopify Plus customer projects.
Key Takeaways
- Shopify Admin GraphQL uses calculated query cost (not request count) — every field has a documented cost weight
- Default rate bucket is 2,000 cost points per app per shop with a 100-points-per-second restore rate
- Use
bulkOperationRunQueryfor any export larger than ~10,000 rows; never page synchronously through large datasetsuserErrorsis the GraphQL convention for business-logic failures — always destructure it on every mutation- Authentication is per-shop access tokens; for embedded apps use session tokens + token exchange (no more OAuth re-prompts)
- API version
2026-01unifies fulfillment APIs and removes the deprecatedOrder.lineItemsconnection — migrate before April 2026
Why GraphQL on Shopify
REST returns whole resources; GraphQL returns exactly the fields you ask for. On a high-volume merchant where a single order has 30 line items, REST forces you to choose between fetching everything or making three sequential round trips. GraphQL collapses that to one. For a typical product detail page on a custom storefront, we measured the request payload drop from 14 KB (REST products.json with fields= filter) to 1.8 KB (GraphQL with explicit field selection), and median latency from 220 ms to 95 ms.
Beyond payload, GraphQL gives you:
- Connections with proper pagination (cursor-based, not page-numbered)
- Bulk operations (async exports of millions of rows for free)
- Predictable cost so you can plan throughput
- Strong typing that drives codegen — we use the official
@shopify/api-codegen-preset
Authentication models
There are three you will encounter in 2026.
1. Public app (App Store) — OAuth + token exchange
The app installs via OAuth, but for the embedded admin experience you no longer redirect the user. Instead, App Bridge issues a session token (a short-lived JWT signed with your client secret), and your backend exchanges that for an offline access token via the token-exchange endpoint. This is documented as "Token Exchange" and replaces the old auth code grant for embedded apps.
// Express handler exchanging a session token for an access token
const res = await fetch(`https://${shop}/admin/oauth/access_token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.SHOPIFY_API_KEY,
client_secret: process.env.SHOPIFY_API_SECRET,
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
subject_token: sessionToken,
subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
requested_token_type: 'urn:shopify:params:oauth:token-type:offline-access-token',
}),
});
2. Custom app (single shop) — admin-issued token
Generated in the merchant's admin under Apps and sales channels > Develop apps. No OAuth dance. The token is bound to the scopes you select and rotates only when the merchant regenerates it. This is what most agencies use for one-off integrations.
3. Shopify Plus partner / multi-tenant — Shop access via Partners API
For agencies serving many Plus shops, you can request shop access via the Partners API and the merchant approves once. Useful for monitoring tools and migration utilities.
The calculated cost model
Every GraphQL query has a declared cost, returned in the extensions.cost block of every response. Default bucket size is 2,000 points; restore rate is 100 points per second. Plus shops get 10x.
query GetProduct($id: ID!) {
product(id: $id) {
title
variants(first: 10) { edges { node { id sku price } } }
}
}
Cost extension returned:
"extensions": {
"cost": {
"requestedQueryCost": 12,
"actualQueryCost": 4,
"throttleStatus": {
"maximumAvailable": 2000,
"currentlyAvailable": 1996,
"restoreRate": 100
}
}
}
The interesting field is actualQueryCost. Connections return first: N * field cost — when N items are not returned, you get refunded the difference. Always read extensions.cost and back off when currentlyAvailable < expectedNextCost * 2. A naive Promise.all of 20 product queries can drain the bucket in 200 ms. We use a token-bucket queue per shop with the limits seeded from the cost extension.
Querying products: a real production example
query ProductPage($handle: String!) {
productByHandle(handle: $handle) {
id
title
descriptionHtml
seo { title description }
images(first: 10) { nodes { url altText width height } }
variants(first: 100) {
nodes {
id
sku
price
compareAtPrice
availableForSale
selectedOptions { name value }
inventoryItem { id measurement { weight { unit value } } }
}
}
metafields(identifiers: [
{ namespace: "custom", key: "warranty_months" },
{ namespace: "shopify", key: "color-pattern" }
]) { key value type }
}
}
A few things worth flagging:
- Use
productByHandleoverproduct(id:)when you have the handle — it avoids a round trip to look up the GID. - Request only the metafields you need. The plural
metafieldsconnection withoutidentifierswill charge per metafield returned. imagesis nownodes-style. The legacyedges { node { ... } }still works but is more verbose.
Mutations and userErrors
Every Shopify mutation returns a userErrors array. A mutation can succeed at the GraphQL transport level (HTTP 200, no errors array) and still have failed business logic.
mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product { id handle title }
userErrors { field message code }
}
}
In TypeScript, our standard mutation wrapper looks like:
async function mutate<T>(query: string, variables: object): Promise<T> {
const res = await client.request<{ data: T; errors?: any; extensions?: any }>(query, variables);
if (res.errors?.length) throw new GraphQLTransportError(res.errors);
// Find any *.userErrors in the response
const root = res.data as any;
for (const op of Object.values(root)) {
if (op && (op as any).userErrors?.length) {
throw new ShopifyUserError((op as any).userErrors);
}
}
return res.data;
}
Without that loop you'll silently drop validation failures. We have seen this bug ship to production three times in client codebases.
Bulk operations for large reads
For exporting more than a few thousand rows, use bulkOperationRunQuery. It runs asynchronously, writes a JSONL file to a Google Cloud Storage URL Shopify gives you, and costs only a flat fee regardless of result size.
mutation {
bulkOperationRunQuery(query: """
{
orders(query: "created_at:>=2026-01-01") {
edges { node {
id name createdAt totalPriceSet { shopMoney { amount currencyCode } }
lineItems { edges { node { id title quantity } } }
}}
}
}
""") {
bulkOperation { id status }
userErrors { field message }
}
}
You then poll currentBulkOperation { id status url } every 5 seconds, and once status is COMPLETED, stream the JSONL from the URL. This is the only sane way to export catalogs over ~10 K SKUs. We have run bulk exports of 2.4 M orders this way for migration projects.
Webhooks and the GraphQL relationship
Webhook subscriptions are also managed via GraphQL (webhookSubscriptionCreate). The payload format itself is still JSON, not GraphQL. For more on reliable delivery, retries, and HMAC verification see our webhook reliability guide.
API versioning and deprecations
Shopify ships a new API version every quarter and supports each version for 12 months. Today's stable is 2026-01. Notable changes:
| Field/Type | Status in 2026-01 | Replacement |
|---|---|---|
Order.lineItems connection | Removed | Order.lineItems(first:) connection (renamed structure) |
FulfillmentOrder union | Stable | — |
Shop.unitSystem | Removed | Shop.weightUnit, Shop.lengthUnit |
Product.priceRangeV2 | Removed | Product.priceRangeV2.minVariantPrice flattened |
Pin your X-Shopify-Api-Version header explicitly. Never let it default to "stable" — Shopify rotates that, and your queries will silently change schema mid-quarter.
Error handling and retries
Three error categories matter:
- HTTP errors (5xx, 429) — retry with exponential backoff. 429 includes a
Retry-Afterheader. - GraphQL errors (in the top-level
errorsarray) — usually permanent (bad query, missing field). Do not retry; alert. - userErrors — business logic failures. Surface to the user; do not retry blindly.
We wrap the client with circuit-breaker semantics: after 5 consecutive 429s within 30 seconds, we pause that shop's queue for a minute. This single change cut our PagerDuty noise on Plus customers by 80%.
Frequently Asked Questions
Is REST going away entirely?
Not yet, but it is frozen. Public apps can no longer use REST for new resources, and Shopify has stated that REST endpoints will not receive new fields. Plan all new development on GraphQL.
How do I generate TypeScript types from the schema?
Use @shopify/api-codegen-preset with graphql-codegen. Point it at your API version and it pulls the schema directly from Shopify's CDN. We regenerate types on every CI run so a schema change at version bump is caught at build time.
What is the difference between cost and rate limit?
Rate limit is the bucket size and restore rate. Cost is what each query subtracts from the bucket. A simple shop query costs 1 point; a deeply nested query with multiple connections can cost 1,000+. Always read extensions.cost.actualQueryCost to plan throughput.
Can I run GraphQL queries from my Liquid theme?
No. The Admin API requires an access token and is server-side only. For storefront queries from theme code, use the Storefront API for headless checkout instead — it has a separate auth model and rate-limit bucket.
How do I handle nested cost when iterating connections?
For each connection level, the cost is first * (1 + nested_cost). A query of 100 products with 100 variants each is 100 * (1 + 100 * (1 + ...)) = 10,000+. Either reduce first, or use bulk operations.
If you're integrating Shopify into Odoo, NetSuite, or a custom backend, ECOSIRE has shipped 60+ Shopify GraphQL integrations across Plus and standard plans. Our Shopify development team handles auth, schema codegen, cost-aware queueing, and bulk-operation pipelines. Reach out via our contact page for a free integration audit.
Geschrieben von
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Skalieren Sie Ihren Shopify-Shop
Maßgeschneiderte Entwicklungs-, Optimierungs- und Migrationsdienste für wachstumsstarken E-Commerce.
Verwandte Artikel
Odoo ORM API Cheat Sheet 2026: Suchen, Lesen, Schreiben, Erstellen
Praktischer Odoo ORM-Spickzettel mit Beispielen: Suchen, Durchsuchen, Lesen, Schreiben, Erstellen, Verknüpfung aufheben, Recordsets, Domänen, berechnete Felder, Leistungstipps.
Odoo Python Decorators Guide: abhängig, eingeschränkt, onchange
Vollständiger Leitfaden für Odoo-Dekoratoren: api.depends, api.constrains, api.onchange, api.ondelete, api.model, api.model_create_multi – wann jeder ausgelöst wird.
Shopify App Bridge 4-Tutorial: Erstellen Sie eingebettete Apps im Jahr 2026
Erstellen Sie in Shopify eingebettete Admin-Apps mit App Bridge 4: Sitzungstoken, Token-Austausch, Navigation, Modalitäten, Ressourcenauswahl und Polaris React 13-Setup.