/admin/api/<version>/graphql.json), validated against Shopify’s published schema for API version 2026-10. It covers products with options, variants, and media, inventory items and levels, customers, orders with transactions, refunds, and cancellation, fulfillment orders, fulfillments, and holds, the shop, apps, staff, and locations, with Shopify’s search syntax in query: arguments, Relay cursor pagination, userErrors, and extensions.cost with a leaky bucket that answers THROTTLED when it runs dry. It also installs apps over OAuth (authorization code grant, offline, online, and expiring tokens, token exchange of session tokens, client credentials) and sends HMAC-signed webhooks. The tests drive it with @shopify/shopify-api (15.0.1) and @shopify/admin-api-client (2.0.1).
At a glance
Start
--service. When it is the only enabled service, it listens on http://localhost:4000. The emulator is one shop, baseline-store.myshopify.com unless shop.domain says otherwise. Without config, the shop has one location, an owner staff member, and a default app (baseline-shopify-app).
Shopify’s SDKs always call https://{shop}. Point them at the emulator with a fetch that swaps the origin. With @shopify/admin-api-client:
@shopify/shopify-api, wrap the runtime’s fetch once after importing the adapter:
URL mapping
Auth
Admin API requests carryX-Shopify-Access-Token.
- Tokens seeded as an app’s
access_tokenact as that app (installed, with itsscopes). - Any other token the CLI accepts acts as the first app with every scope, so a quick start needs no setup.
- An unknown, revoked, or expired token answers HTTP 401
{"errors":"[API] Invalid API key or access token (unrecognized login or wrong password)"}. - A field outside the token’s scopes answers
ACCESS_DENIED(Access denied for productCreate field. Required access: \write_products` access scope.`). A write scope includes its read scope.
API versions
Every supported version (the four stable ones, the release candidate, andunstable) is served from the pinned 2026-10 schema. An older version falls forward to the oldest supported version, as Shopify does; a version that is not a quarterly release (2026-05, latest) or is not out yet answers 404 {"errors":"Not Found"}. X-Shopify-API-Version on every response names the version that answered. /admin/api/graphql.json without a version is served as the oldest supported version.
The schema is vendored from Shopify’s @shopify/dev-mcp package (ISC). A weekly drift check moves the pin when Shopify releases a new stable version.
OAuth
Apps install the way they do on a real shop.shopify.auth.begin, auth.callback, auth.tokenExchange, auth.refreshToken, auth.clientCredentials, and auth.migrateToExpiringToken all work against the emulator.
GET /admin/oauth/authorize— Install screen: the app, the scopes it asks for, and a button per staff member.client_idmust be a seeded app andredirect_urione of itsredirect_urls(or under itsapplication_urlwhen it has none).grant_options[]=per-userasks for an online token.POST /admin/oauth/authorize— Approve install: installs the app with the scopes and redirects toredirect_uriwithcode,host,shop,state,timestamp, andhmac.POST /admin/oauth/access_token— Get access token: JSON or form body withclient_idandclient_secret. Grants below.GET /admin/oauth/access_scopes.json— List access scopes:{"access_scopes":[{"handle":"write_products"}]}for the token inX-Shopify-Access-Token.GET /admin/apps/<api key or handle>— Open app: as the Shopify admin does, a redirect to itsapplication_urlwith the signed query, plusembedded=1,id_token(a session token),session, andlocalefor embedded apps. An app that is not installed yet is installed with its configured scopes first (managed installation).?staff=<email>picks the user.
- Authorization code:
codefrom the redirect, once, within ten minutes. An offline token (shpat_) never expires; withexpiring=1it lasts an hour and comes with arefresh_tokenthat lasts 90 days. An online token (shpua_) lasts a day and carriesassociated_userandassociated_user_scope. - Token exchange (
grant_type=urn:ietf:params:oauth:grant-type:token-exchange): a session token assubject_tokenwithsubject_token_type=urn:ietf:params:oauth:token-type:id_token, for an online or offline token (requested_token_type). The session token must be signed with the app’s client secret (HS256), be for the app’s client ID (aud) and this shop (dest), and not be expired. A non-expiring offline token assubject_tokenwith the offline token type migrates it to an expiring one and revokes the old token. - Refresh token (
grant_type=refresh_token): a new expiring offline token; the old token and refresh token stop working. - Client credentials (
grant_type=client_credentials): a 24 hour offline token for an installed app.
{"error":"invalid_subject_token","error_description":"Session token signature is invalid"}, with 401 for a wrong client secret and 400 otherwise.
Shopify signs the queries it sends to an app with hmac: the hex HMAC-SHA256 of the other parameters, sorted by name and URL encoded, keyed with the client secret. shopify.utils.validateHmac accepts the emulator’s.
GraphQL
Queries are validated against the schema and answered with graphql-ruby’s wording (Field 'nope' doesn't exist on type 'Shop', Field 'product' is missing required arguments: id), on HTTP 200.
- Products:
products,productsCount,product,productByHandle,productByIdentifier,productVariants,productVariantsCount,productVariant,inventoryItem. - Product mutations:
productCreate(withproductOptionsandmedia),productUpdate,productDelete,productVariantsBulkCreate(with theREMOVE_STANDALONE_VARIANTandPRESERVE_STANDALONE_VARIANTstrategies),productVariantsBulkUpdate,productVariantsBulkDelete,productOptionsCreate. Problems come back inuserErrorswith Shopify’s messages and codes (Title can't be blank,VARIANT_ALREADY_EXISTS,CANNOT_DELETE_LAST_VARIANT). - Customers:
customers,customersCount,customer,customerByIdentifier,customerCreate,customerUpdate,customerDelete,customerAddressCreate,customerAddressUpdate,customerAddressDelete,customerUpdateDefaultAddress. - Orders:
orders,ordersCount,order,orderByIdentifier,orderCreate,orderUpdate,orderCancel,orderClose,orderOpen,orderMarkAsPaid,orderCapture,refundCreate,tagsAdd,tagsRemove. - Fulfillment:
fulfillmentOrders,fulfillmentOrder,fulfillment,fulfillmentCreate,fulfillmentCancel,fulfillmentTrackingInfoUpdate,fulfillmentOrderHold,fulfillmentOrderReleaseHold. - Shop:
shop,app,appInstallation,currentAppInstallation,currentStaffMember,staffMember,locations,locationsCount,location,node,nodes,job,appUninstall. - Webhooks:
webhookSubscriptions,webhookSubscriptionsCount,webhookSubscription,webhookSubscriptionCreate,webhookSubscriptionUpdate,webhookSubscriptionDelete. See Webhooks.
Title option and its Default Title variant, or with the first value of each option given. Handles come from the title and get -1, -2 suffixes when taken. Media accepts IMAGE and EXTERNAL_VIDEO sources and is READY at once.
Customers
A customer needs a name, an email address, or a phone number (A name, phone number, or email address must be present). Emails are stored lowercase and, like phone numbers (kept in E.164), are unique (Email has already been taken). A customer with orders cannot be deleted. numberOfOrders, amountSpent (payments received less refunds), lastOrder, and orders come from the customer’s orders.
Orders
orderCreate takes line items for variants (title, SKU, vendor, and price come from the variant unless given) or custom line items with a title and priceSet, shipping lines, tax lines (with a priceSet, or a rate applied to the line total), transactions, financialStatus, fulfillmentStatus or fulfillment, addresses, tags, and a customer to associate (toAssociate by ID or email) or upsert (toUpsert). Orders are numbered from #1001. With options.inventoryBehaviour set to DECREMENT_IGNORING_POLICY or DECREMENT_OBEYING_POLICY, the variants’ inventory goes down, and the second refuses (INVENTORY_CLAIM_FAILED) when a DENY variant lacks stock.
The financial status is derived from the transactions, as Shopify does: PAID once sales and captures cover the total, PARTIALLY_PAID before that, AUTHORIZED with uncaptured authorizations, REFUNDED or PARTIALLY_REFUNDED once refunds exist, VOIDED when the authorizations were voided, else PENDING. A financialStatus of PAID or AUTHORIZED without transactions records a manual sale or authorization for the total; other statuses are kept as given until a transaction is recorded.
orderCapturecaptures part or all of an authorization (finalCapturevoids the rest) and refuses more than is left.orderMarkAsPaidrecords a manual sale for the outstanding amount.refundCreaterefunds line items (RETURN,CANCEL, andLEGACY_RESTOCKput stock back), shipping, and money throughREFUNDtransactions, up to the net payment received.orderCancelreturns aJob(done when polled withjob), and withrefundMethod: { originalPaymentMethodsRefund: true }refunds the net payment, voids open authorizations, restocks when asked, closes the order and its fulfillment orders, and setscancelledAt,cancelReason, andcancellation.staffNote. Orders with fulfillments cannot be cancelled.orderCloseandorderOpenarchive and unarchive; a cancelled order cannot be reopened.
Fulfillment orders
Each order gets one fulfillment order at the shop’s first location.fulfillmentCreate fulfills whole fulfillment orders or chosen quantities (Invalid fulfillment order line item quantity requested.), with tracking; a fulfillment order with nothing left is CLOSED, a partly fulfilled one IN_PROGRESS. fulfillmentCancel gives the quantities back. fulfillmentOrderHold puts a fulfillment order ON_HOLD with a reason and handle (unique per app, at most 10 holds); holding only some quantities splits the rest into a new fulfillment order, returned as remainingFulfillmentOrder. fulfillmentOrderReleaseHold releases some or all holds. The order’s displayFulfillmentStatus follows: UNFULFILLED, ON_HOLD, IN_PROGRESS, PARTIALLY_FULFILLED, FULFILLED.
Orders search on name, status (open, closed, cancelled, not_closed, any), financial_status, fulfillment_status (shipped, partial, unshipped, unfulfilled, on_hold), email, tag, tag_not, customer_id, sku, test, source_name, po_number, confirmation_number, created_at, updated_at, and processed_at. Customers search on email, phone, first_name, last_name, state, tag, tag_not, country, accepts_marketing, created_at, and updated_at.
Order reads need read_orders, customer reads read_customers, and fulfillment orders one of the read_*_fulfillment_orders scopes (write_merchant_managed_fulfillment_orders to fulfill or hold).
Unmodeled fields
Fields Baseline does not model answer as an empty shop would:metafield is null and lists and connections are empty. Any other root field answers NOT_EMULATED with its name.
Pagination
Connections takefirst or last (one is required, at most 250) with after and before cursors, sortKey, and reverse, and return nodes, edges with cursors, and pageInfo.
Search syntax
query: arguments take Shopify’s search syntax: free text, field:value, field:<10, field:>=2024-01-01, field:* (has a value), trailing * prefixes, comma lists (status:active,draft), AND, OR, NOT, a leading -, parentheses, and quoted phrases. A field the resource does not support filters nothing and is reported in extensions.search:
title, handle, vendor, product_type, status, tag, tag_not, sku, barcode, gift_card, inventory_total, price, variant_id, created_at, updated_at, published_at, published_status, and out_of_stock_somewhere.
Query cost
Every response reports the cost:first (or last) times their nodes’ cost, and mutations 10. The requested cost is taken from the bucket before the query runs, and the difference from the actual cost is returned after. When the bucket holds less than the requested cost, the response is {"errors":[{"message":"Throttled","extensions":{"code":"THROTTLED",...}}]}. A query over 1000 points answers MAX_COST_EXCEEDED. Set the bucket with shop.throttle.
Webhooks
Subscribe withwebhookSubscriptionCreate (uri, or the older callbackUrl), shopify.webhooks.register, or the webhooks list on a seeded app. When a request changes something, each matching subscription of an installed app gets one POST of the topic’s REST-shaped JSON payload with these headers:
shopify.webhooks.validate accepts these deliveries. Topics sent: products/create, products/update, products/delete, customers/create, customers/update, customers/delete, orders/create, orders/updated, orders/paid, orders/cancelled, orders/fulfilled, orders/partially_fulfilled, order_transactions/create, refunds/create, fulfillments/create, fulfillments/update, fulfillment_orders/placed_on_hold, fulfillment_orders/hold_released, and app/uninstalled. Other topics can be subscribed to and are never sent.
includeFieldscuts the payload to the named fields; nested fields use dots (line_items.title).filtertakes the search syntax over the payload’s fields, with dots for nested fields (total_price:>100,variants.price:>=10.00). A payload that does not match is not sent.- A subscription needs the topic’s read scope (
read_ordersfor orders, transactions, refunds, and fulfillments), elseuserErrorsexplains which scope is missing. The same address twice for a topic isAddress for this topic has already been taken. Pub/Sub and EventBridge addresses and the XML format are refused. - A delivery fails when the address does not answer 2xx within five seconds. Failed deliveries are retried eight times, after 1, 2, 4, 8, 16, 32, 60, and 60 seconds (Shopify spreads them over four hours).
- An app sees and changes only its own subscriptions.
- Uninstalling an app (
appUninstall, or the inspector’s Uninstall button) sends itsapp/uninstalledwebhooks with the shop as payload, then revokes its tokens and deletes its subscriptions.
Seed config
access_token is installed with that offline token, as a custom app is; without one it is installed through OAuth. webhooks topics are written as orders/create or ORDERS_CREATE.
Seeded order line items name a variant by sku or give a title and price. Orders are seeded only into an empty store, so a restored state is not seeded twice.
Inspector
GET /_inspector shows products with their variants and inventory, customers, orders with their payment and fulfillment status and holds, apps with their granted scopes and tokens (with Open app and Uninstall buttons), webhook subscriptions and deliveries with their status and retries, and the shop settings, locations, and staff.
Limits
@shopify/shopify-api talks to the Admin API through GraphQL; its REST resources are deprecated and need restResources to be passed in. Baseline serves no REST Admin resources; the only REST calls the SDK makes are the OAuth ones above, which are served.
Conformance
See CONFORMANCE.md for every provider.