← Back to Blogs

How to Call the Shopify GraphQL Admin API

How to Call the Shopify GraphQL Admin API (with cURL Examples)
Developer Guide

How to Call the Shopify GraphQL Admin API

A hands-on walkthrough with cURL: fetch an order, fetch a product, update inventory, and create a fulfillment — the four calls every Shopify integration ends up making.

By the AllSync Integration Team · Reading time: 10 minutes

If you're connecting Shopify to a POS, an ERP like SAP Business One, a warehouse system, or a marketplace channel, sooner or later you're writing code against Shopify's Admin API. Shopify's REST Admin API is in maintenance mode for most new resources, and the GraphQL Admin API is now the recommended way to read and write store data — it lets you ask for exactly the fields you need in a single request instead of stitching together several REST calls.

This guide walks through four calls that cover most integration workloads: reading an order, reading a product, adjusting inventory, and creating a fulfillment. Every example uses plain curl, so you can adapt it to whatever language your integration is built in.

1. Authentication and request setup

Every request goes to a single endpoint per store, regardless of which query or mutation you're calling:

Endpoint
POST https://{store}.myshopify.com/admin/api/{version}/graphql.json

Replace {store} with your shop's .myshopify.com handle. Replace {version} with a supported API version string, e.g. 2026-01 — Shopify ships a new stable version every quarter (January, April, July, October), so check shopify.dev/changelog for the current one before you go to production, and pin your integration to a specific version rather than tracking unstable.

You'll need an Admin API access token. For a private, store-specific integration, create one under Settings → Apps and sales channels → Develop apps in the Shopify admin, grant it only the scopes it needs (for the calls below: read_orders, read_products, write_inventory, and write_merchant_managed_fulfillment_orders), and install the app to generate a token that starts with shpat_. Public apps distributed through the App Store instead go through OAuth to obtain a token per merchant.

Every request needs these two headers:

Header Value
Content-Type application/json
X-Shopify-Access-Token your Admin API access token
Tip: the examples below pipe the JSON body in through a heredoc (-d @- <<'EOF') instead of cramming everything onto one -d '...' line. It's the easiest way to keep a multi-line GraphQL query readable and avoid shell-quoting headaches.

2. Example: get an order

Use the order query with the order's GraphQL ID (gid://shopify/Order/...), or the orders query with a search filter if you only have the order number.

Fetch a single order by ID

curl -X POST "https://your-store.myshopify.com/admin/api/2026-01/graphql.json" \
  -H "Content-Type: application/json" \
  -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
  -d @- <<'EOF'
{
  "query": "query GetOrder($id: ID!) { order(id: $id) { name createdAt displayFinancialStatus displayFulfillmentStatus totalPriceSet { shopMoney { amount currencyCode } } customer { firstName lastName email } lineItems(first: 20) { edges { node { title quantity sku variant { id price } } } } } }",
  "variables": { "id": "gid://shopify/Order/6000728723510" }
}
EOF

Look up an order by its order number

If all you have is the order number shown to the customer (e.g. #1017), search the orders connection instead:

curl -X POST "https://your-store.myshopify.com/admin/api/2026-01/graphql.json" \
  -H "Content-Type: application/json" \
  -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
  -d @- <<'EOF'
{
  "query": "query FindOrder($q: String!) { orders(first: 5, query: $q) { edges { node { id name displayFulfillmentStatus } } } }",
  "variables": { "q": "name:#1017" }
}
EOF
By default the Admin API only returns orders from the last 60 days. Reading older orders requires the read_all_orders scope and Shopify's approval for that scope on public apps.

3. Example: get a product

Products and their variants (SKU, price, inventory item) come back in one call — no need for a separate variants endpoint like in REST.

curl -X POST "https://your-store.myshopify.com/admin/api/2026-01/graphql.json" \
  -H "Content-Type: application/json" \
  -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
  -d @- <<'EOF'
{
  "query": "query GetProduct($id: ID!) { product(id: $id) { id title vendor productType status variants(first: 25) { edges { node { id title sku price inventoryQuantity inventoryItem { id } } } } } }",
  "variables": { "id": "gid://shopify/Product/8123456789012" }
}
EOF

To search instead of looking up by ID — useful when your ERP only knows the SKU — use the products connection with a query filter:

curl -X POST "https://your-store.myshopify.com/admin/api/2026-01/graphql.json" \
  -H "Content-Type: application/json" \
  -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
  -d @- <<'EOF'
{
  "query": "query FindBySku($q: String!) { products(first: 5, query: $q) { edges { node { id title variants(first: 10) { edges { node { sku inventoryItem { id } } } } } } } }",
  "variables": { "q": "sku:AS-1001-BLK" }
}
EOF

Note the inventoryItem { id } field on each variant — you'll need that ID for the next step.


4. Example: update inventory

Inventory in Shopify is tracked per inventory item per location, so a stock update needs three pieces of information: the inventoryItemId (from the product query above), the locationId, and the new quantity.

Step 1 — get your location IDs

curl -X POST "https://your-store.myshopify.com/admin/api/2026-01/graphql.json" \
  -H "Content-Type: application/json" \
  -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
  -d '{"query": "{ locations(first: 10) { edges { node { id name } } } }"}'

Step 2 — set the on-hand quantity

Use inventorySetQuantities to set an absolute quantity (this is what you want when your source of truth is an external WMS or ERP pushing a known stock count). Set name to "available" for sellable stock:

curl -X POST "https://your-store.myshopify.com/admin/api/2026-01/graphql.json" \
  -H "Content-Type: application/json" \
  -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
  -d @- <<'EOF'
{
  "query": "mutation SetInventory($input: InventorySetQuantitiesInput!) { inventorySetQuantities(input: $input) { inventoryAdjustmentGroup { reason changes { name delta } } userErrors { field message code } } }",
  "variables": {
    "input": {
      "name": "available",
      "reason": "correction",
      "ignoreCompareQuantity": true,
      "referenceDocumentUri": "allsync://sync/2026-09-09T08:00:00Z",
      "quantities": [
        {
          "inventoryItemId": "gid://shopify/InventoryItem/30322695",
          "locationId": "gid://shopify/Location/124656943",
          "quantity": 42
        }
      ]
    }
  }
}
EOF

If you're adding or subtracting stock relative to what's already there (a delta, e.g. "5 units just sold in the POS") rather than pushing an absolute count, use inventoryAdjustQuantities instead — same shape, but each entry takes a delta instead of a quantity.

Tip: setting ignoreCompareQuantity: false and passing a compareQuantity lets Shopify reject the write if the stock changed since you last read it — a cheap optimistic-locking guard when two systems might update the same SKU at once.

5. Example: update a fulfillment (mark an order as shipped)

Fulfillment is the one place where the Admin API forces you through an extra concept: a fulfillment order. Every order line item belongs to a fulfillment order (Shopify splits it automatically per location or fulfillment service), and you create the actual shipment against that, not against the order directly.

Step 1 — get the fulfillment order ID

curl -X POST "https://your-store.myshopify.com/admin/api/2026-01/graphql.json" \
  -H "Content-Type: application/json" \
  -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
  -d @- <<'EOF'
{
  "query": "query GetFulfillmentOrders($id: ID!) { order(id: $id) { fulfillmentOrders(first: 5) { edges { node { id status } } } } }",
  "variables": { "id": "gid://shopify/Order/6000728723510" }
}
EOF

Step 2 — create the fulfillment with tracking info

Use fulfillmentCreate (its predecessor, fulfillmentCreateV2, is deprecated in favor of this one). Omitting fulfillmentOrderLineItems for an entry fulfills every remaining line item on that fulfillment order, which is usually what you want for a single-location shipment:

curl -X POST "https://your-store.myshopify.com/admin/api/2026-01/graphql.json" \
  -H "Content-Type: application/json" \
  -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
  -d @- <<'EOF'
{
  "query": "mutation CreateFulfillment($fulfillment: FulfillmentInput!) { fulfillmentCreate(fulfillment: $fulfillment) { fulfillment { id status trackingInfo { company number url } } userErrors { field message } } }",
  "variables": {
    "fulfillment": {
      "lineItemsByFulfillmentOrder": [
        { "fulfillmentOrderId": "gid://shopify/FulfillmentOrder/10079785100" }
      ],
      "notifyCustomer": true,
      "trackingInfo": {
        "company": "DHL Express",
        "number": "1234567890",
        "url": "https://www.dhl.com/track?id=1234567890"
      }
    }
  }
}
EOF

To ship only part of an order — a partial fulfillment — include specific line items under fulfillmentOrderLineItems, each with its own id and quantity, instead of leaving the array empty.


6. Reading errors correctly

GraphQL responses can fail in two different ways, and integrations that only check one of them tend to fail silently:

  • Top-level errors — malformed queries, invalid IDs, missing scopes, or auth failures. If this array is present, the mutation likely didn't run at all.
  • userErrors inside the payload — the request was valid GraphQL, but Shopify rejected it for a business reason (e.g. negative inventory, an already-fulfilled order, an invalid location). Every mutation above returns this array, and it's empty on success — always check its length, not just HTTP status, since Shopify returns 200 OK even when userErrors is populated.

Also watch the extensions.cost block that comes back with every response. GraphQL calls are metered by query cost rather than a flat request count; if throttleStatus.currentlyAvailable is running low, back off before you hit a THROTTLED error.


7. A few production notes

These four calls work fine as written for a script or a one-off test. For a system that runs unattended against live orders, a few things are worth building in from day one: retry mutations with exponential backoff on THROTTLED responses; use referenceDocumentUri or your own idempotency key so a retried inventory write doesn't double-apply; prefer webhooks (orders/create, inventory_levels/update) over polling so you're not burning query cost checking for changes that haven't happened; and pin an explicit API version in every request so a quarterly Shopify release doesn't silently change field behavior under you.

Building a Shopify integration for your business?

AllSync connects Shopify to the systems retail, F&B, hospitality, and e-commerce businesses across Southeast Asia and APAC already run on — POS platforms, SAP Business One, NetSuite, Business Central, and marketplaces like Lazada, Shopee, and TikTok Shop — with orders, inventory, and fulfillment kept in sync automatically. No custom API code required on your end.

Talk to the AllSync team

Written by the AllSync Integration Team. Have a question about connecting Shopify to your other systems? Get in touch — we build this for a living.

← Previous PostNext Post →