Developer reference

Shopify GraphQL for Variants — Admin API Reference

The Admin API is where you create, update, and manage variants at scale. This is the full 2026 reference — mutations, bulk operations, inventory, webhooks, and the migration path from the deprecated REST product endpoints.

Last updated: August 14, 2026~11 min readBy the Print It My Way team

Why GraphQL, not REST

Shopify announced REST Product endpoints deprecated for new apps on February 1, 2025. All new variant CRUD work should use GraphQL. Existing REST integrations continue to function but won't receive new features. If you're starting a project in 2026, only GraphQL. The 2025-07 Admin API version was the last to add REST-only fields; nothing new since.

The upside: GraphQL lets you request exactly the fields you need, chain related data (product + variants + inventory levels + metafields) in one request, and dramatically reduce the round-trips of a REST equivalent.

Authentication + endpoint

All Admin GraphQL calls hit:

https://{shop}.myshopify.com/admin/api/2025-07/graphql.json

Headers:

For custom apps installed on a single store, generate the token in the store admin (Settings → Apps → Develop apps → API credentials). For public apps distributed on the App Store, tokens come from the OAuth flow; scope your app to write_products + read_products + write_inventory as needed.

Query variants on a product

query ProductVariants($id: ID!) {
  product(id: $id) {
    id
    title
    options { name values }
    variants(first: 100) {
      nodes {
        id
        title
        sku
        barcode
        price
        compareAtPrice
        inventoryQuantity
        inventoryPolicy
        weight
        weightUnit
        selectedOptions { name value }
        image { url altText }
        inventoryItem {
          id
          tracked
          inventoryLevels(first: 5) {
            nodes { location { name } quantities(names: ["available"]) { quantity } }
          }
        }
      }
      pageInfo { hasNextPage endCursor }
    }
  }
}

Variables: { "id": "gid://shopify/Product/1234567890" }. GIDs (globally unique IDs) are the standard identifier in Admin API — the numeric ID from the URL wrapped in gid://shopify/Product/{id}.

Create a product with variants

The modern pattern uses productCreate + productSet. As of 2025-01 API, productSet is the recommended mutation for defining a full product state (variants, options, media) in one atomic operation:

mutation ProductSet($input: ProductSetInput!) {
  productSet(input: $input) {
    product {
      id
      variants(first: 20) { nodes { id sku price selectedOptions { name value } } }
    }
    userErrors { field message code }
  }
}

Variables:

{
  "input": {
    "title": "Cotton T-Shirt",
    "productOptions": [
      { "name": "Size",  "values": [{"name":"S"},{"name":"M"},{"name":"L"},{"name":"XL"}] },
      { "name": "Color", "values": [{"name":"Red"},{"name":"Blue"},{"name":"Black"}] }
    ],
    "variants": [
      { "optionValues": [{"optionName":"Size","name":"S"},{"optionName":"Color","name":"Red"}],  "price":"25.00", "sku":"TSHIRT-S-RED",  "inventoryQuantities":[{"locationId":"gid://shopify/Location/123","name":"available","quantity":50}] },
      { "optionValues": [{"optionName":"Size","name":"M"},{"optionName":"Color","name":"Red"}],  "price":"25.00", "sku":"TSHIRT-M-RED",  "inventoryQuantities":[{"locationId":"gid://shopify/Location/123","name":"available","quantity":50}] }
    ]
  }
}

Fields to know:

The response includes userErrors — always check this before assuming success. Common validation errors: duplicate SKU (uniqueness enforced per shop), missing option value, exceeding the 2,048 variants per product cap.

Update a variant's price

mutation UpdatePrice($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
  productVariantsBulkUpdate(productId: $productId, variants: $variants) {
    productVariants { id price }
    userErrors { field message }
  }
}
{
  "productId": "gid://shopify/Product/1234567890",
  "variants": [
    { "id": "gid://shopify/ProductVariant/40123456789", "price": "27.99" },
    { "id": "gid://shopify/ProductVariant/40123456790", "price": "27.99" }
  ]
}

Update up to 250 variants per mutation call. For bulk price changes across all products, prefer Bulk Operations (below).

Update inventory

Inventory sits on inventoryItem, one per variant. Adjustments happen per location:

mutation AdjustInventory($input: InventoryAdjustQuantitiesInput!) {
  inventoryAdjustQuantities(input: $input) {
    inventoryAdjustmentGroup { reason changes { name delta } }
    userErrors { field message }
  }
}
{
  "input": {
    "reason": "correction",
    "name": "available",
    "changes": [
      { "delta": -5, "inventoryItemId": "gid://shopify/InventoryItem/44123456", "locationId": "gid://shopify/Location/123" }
    ]
  }
}

For absolute set (not delta), use inventorySetQuantities. Reason codes: correction, cycle_count_available, damaged, movement_created, received, restock, safety_stock, shrinkage.

Bulk operations for 1,000+ variants

Any operation that returns or mutates more than 250 records at once belongs in Bulk Operations — async, no rate limit, better throughput. Two-step pattern:

Step 1: submit bulk query

mutation {
  bulkOperationRunQuery(query: """
    {
      products {
        edges {
          node {
            id
            title
            variants { edges { node { id sku price inventoryQuantity } } }
          }
        }
      }
    }
  """) {
    bulkOperation { id status }
    userErrors { field message }
  }
}

Step 2: poll for completion + download the JSONL result

query { currentBulkOperation { id status url errorCode objectCount } }

When status: COMPLETED, download from url — it's a newline-delimited JSON file with one product/variant per line. Parse with any streaming JSONL parser.

For bulk mutations (mass import, mass update), use bulkOperationRunMutation with a JSONL file uploaded to a staged upload target. Full round-trip in Shopify's docs.

Rate limits — the cost bucket

Admin GraphQL is cost-based, not request-count-based. Each query has a computed cost; you have a per-shop bucket that refills over time:

PlanBucket sizeRestore rate
Standard1,000 points50/sec
Advanced2,000 points100/sec
Plus10,000 points500/sec
EnterpriseCustomCustom

Every response includes extensions.cost:

"extensions": {
  "cost": {
    "requestedQueryCost": 51,
    "actualQueryCost": 22,
    "throttleStatus": {
      "maximumAvailable": 1000,
      "currentlyAvailable": 978,
      "restoreRate": 50
    }
  }
}

On THROTTLED error, wait Math.ceil((cost - currentlyAvailable) / restoreRate) seconds before retrying. Real production apps: wrap your GraphQL client with automatic throttle handling. The official @shopify/shopify-api Node library does this out of the box.

Webhooks for variant changes

Subscribe to PRODUCTS_UPDATE to get pushed variant changes in real-time (no polling required):

mutation {
  webhookSubscriptionCreate(
    topic: PRODUCTS_UPDATE,
    webhookSubscription: {
      callbackUrl: "https://your-app.example.com/webhooks/products",
      format: JSON
    }
  ) {
    webhookSubscription { id }
    userErrors { field message }
  }
}

Verify the HMAC signature on incoming webhooks (SHA-256 with your app secret). Return 200 within 5 seconds or Shopify retries — offload heavy processing to a background queue.

For inventory-only changes, subscribe to INVENTORY_LEVELS_UPDATE instead — smaller payload, faster processing.

Testing without a live store

Use a Shopify Partners development store — free, unlimited, populated with sample data. Create at partners.shopify.com. Full Admin API access, no plan required. Perfect for CI test suites — you can spin up a dev store per test run if needed.

Common errors

ErrorCauseFix
Field 'productVariantCreate' doesn't existDeprecated in favor of productVariantsBulkCreateSwitch to bulk variant create + productSet
422 Unprocessable Entity: SKU already takenSKU uniqueness enforced per shopPrepend product ID or unique suffix
THROTTLEDCost bucket emptyWait based on throttleStatus; add caching
ACCESS_DENIED on inventory mutationMissing write_inventory scopeUpdate app scopes + re-authorize
Bulk operation stuck in CREATEDAnother bulk op runningOnly one bulk op per shop at a time; wait or cancel

Best Node.js library

Use @shopify/shopify-api (official). Handles OAuth, GraphQL, webhook verification, session storage, and throttle backoff automatically:

import { shopifyApi } from '@shopify/shopify-api';

const shopify = shopifyApi({
  apiKey: process.env.SHOPIFY_API_KEY,
  apiSecretKey: process.env.SHOPIFY_API_SECRET,
  scopes: ['write_products', 'read_inventory'],
  hostName: 'your-app.example.com',
  apiVersion: '2025-07',
});

const client = new shopify.clients.Graphql({ session });
const response = await client.query({ data: { query, variables } });

Frequently asked questions

Should I use GraphQL or REST for Shopify variants?

GraphQL. REST product endpoints are deprecated for new apps since Feb 2025. Same capabilities, better efficiency, ongoing feature investment.

How do I create a product with variants via API?

Use productSet mutation with productOptions + variants arrays. Atomic — all variants created in one request. Response includes userErrors array to check.

How do I bulk import 1,000+ variants?

Bulk Operations API. Submit bulkOperationRunMutation with a JSONL file uploaded to staged upload target. Async — poll for completion. No rate limit but slower per-item throughput.

How do I update variant price?

productVariantsBulkUpdate — up to 250 variants per call. Pass productId + array of {id, price} objects.

How do I update inventory?

inventoryAdjustQuantities for delta changes, inventorySetQuantities for absolute values. Include valid reason code. Per-location per-variant.

What's the Admin API rate limit?

Cost-based bucket. Standard: 1,000 points, 50/sec restore. Plus: 10,000 points, 500/sec restore. Check extensions.cost.throttleStatus in every response.

How do I subscribe to variant change webhooks?

webhookSubscriptionCreate mutation with topic PRODUCTS_UPDATE (variants included) or INVENTORY_LEVELS_UPDATE (inventory only, smaller payload).

Best library for Shopify Admin API in Node?

@shopify/shopify-api — official, handles OAuth, GraphQL, webhooks, session, throttle backoff.

How do I test without a live store?

Shopify Partners dev store — free, unlimited, full API access. Create at partners.shopify.com.

How do I handle throttling errors?

Wait based on throttleStatus.currentlyAvailable vs required cost. Wrap client in exponential backoff. Use bulk operations for large jobs.

Related developer references

Building a Shopify app with variants?

Print It My Way is an example of a Shopify app doing exactly this — variants + line-item properties + Cart Transform Functions. Free plan available for merchants.

Install Print It My Way