Skip to content

Admin GraphQL API ​

GraphQL only (for new apps) ​

Since 1 April 2025, new public apps submitted to the App Store must use the GraphQL Admin API. REST Admin is legacy. It still works for existing apps but gets no new features. Learn GraphQL; don't start with REST.

Versioning ​

  • Endpoint: https://{shop}.myshopify.com/admin/api/{version}/graphql.json
  • A new version ships every quarter, on the first day of the quarter: 2026-01, 2026-04, 2026-07, 2026-10…
  • Each version is supported for at least 12 months. 2026-10 is current stable, and 2027-01 is the release candidate.
  • Calling a retired version silently falls forward to the oldest supported one, which can break you. Bump your version every quarter or two.

Your first query ​

ts
const { admin } = await authenticate.admin(request);

const res = await admin.graphql(
  `#graphql
  query RecentOrders($first: Int!) {
    orders(first: $first, sortKey: CREATED_AT, reverse: true) {
      edges {
        node {
          id
          name
          createdAt
          totalPriceSet { shopMoney { amount currencyCode } }
          customer { displayName }
        }
      }
      pageInfo { hasNextPage endCursor }
    }
  }`,
  { variables: { first: 10 } },
);
const { data } = await res.json();

Mutations and userErrors ​

ts
const res = await admin.graphql(
  `#graphql
  mutation SetNote($input: OrderInput!) {
    orderUpdate(input: $input) {
      order { id note }
      userErrors { field message }
    }
  }`,
  { variables: { input: { id: "gid://shopify/Order/123", note: "Sent WhatsApp confirmation" } } },
);

Always check userErrors

A mutation that fails validation still returns HTTP 200. The failure is in userErrors. Not checking it is the #1 silent bug.

IDs are GIDs ​

Shopify IDs look like gid://shopify/Product/1234567890. Store the full GID. Webhook payloads often include both id (numeric) and admin_graphql_api_id (GID).

Pagination ​

Cursor-based: pass after: endCursor while hasNextPage is true. Page size is at most 250.

Rate limits: cost-based ​

Each query has a calculated cost. Your app has a bucket of points that refills every second, and the bucket is bigger on Plus. The response includes:

json
"extensions": { "cost": { "requestedQueryCost": 12, "actualQueryCost": 8,
  "throttleStatus": { "maximumAvailable": 2000, "currentlyAvailable": 1992, "restoreRate": 100 } } }

If you are throttled, you get a THROTTLED error. Back off and retry.

Bulk operations ​

For large exports, such as all orders for a year, use bulkOperationRunQuery. Shopify runs the query asynchronously and gives you a JSONL file URL. Subscribe to the bulk_operations/finish webhook rather than polling.

Metafields & metaobjects ​

  • Metafields: custom fields on any resource (namespace.key). Use the $app namespace for app-owned data.
  • Metaobjects: your own custom content types, stored in Shopify.

These are often a better place for per-shop settings than your own database.

Tools ​

  • GraphiQL: shopify app dev gives you a GraphiQL explorer (press g) authenticated against your dev store.
  • Codegen: the template ships @shopify/api-codegen-preset. Run npm run graphql-codegen for typed queries.

Project step: list the 10 most recent orders on your app's home page.

Check yourself ​

  1. A mutation returns 200 but nothing changed. Where do you look?
  2. How do you export 500k orders without hitting rate limits?