Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Query Shopify Admin/Storefront GraphQL APIs via curl.
.claude/skills/nousresearch-shopify/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-16 | ✗→✓ | ▲ Improved | 121% | 0% |
| case-22 | ✓→✓ | = Same ✓ | 177% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 325% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 184% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 228% | 0% |
Work with Shopify stores directly through curl: list products, manage inventory, pull orders, update customers, read metafields. No SDK, no app framework — just the GraphQL endpoint and a custom-app access token.
The REST Admin API is legacy since 2024-04 and only receives security fixes. Use GraphQL Admin for all admin work. Use Storefront GraphQL for read-only customer-facing queries (products, collections, cart).
shpat_.${HERMES_HOME:-~/.hermes}/.env: SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxx SHOPIFY_STORE_DOMAIN=my-store.myshopify.com SHOPIFY_API_VERSION=2026-01
> Heads up: As of January 1, 2026, new "legacy custom apps" created in the Shopify admin are gone. New setups should use the Dev Dashboard (shopify.dev/docs/apps/build/dev-dashboard). Existing admin-created apps keep working. If the user's shop has no existing custom app and it's after 2026-01-01, direct them to Dev Dashboard instead of the admin flow.
Common scopes by task:
read_products, write_productsread_inventory, write_inventory, read_locationsread_orders, write_orders (30 most recent without read_all_orders)read_customers, write_customersread_draft_orders, write_draft_ordersread_fulfillments, write_fulfillmentshttps://$SHOPIFY_STORE_DOMAIN/admin/api/$SHOPIFY_API_VERSION/graphql.jsonX-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN (NOT Authorization: Bearer)POST, always Content-Type: application/json, body is {"query": "...", "variables": {...}}errors array and per-field userErrors. Always check both.gid://shopify/Product/10079467700516, gid://shopify/Variant/..., gid://shopify/Order/.... Pass these verbatim — don't strip the prefix.extensions.cost with requestedQueryCost, actualQueryCost, throttleStatus.{currentlyAvailable, maximumAvailable, restoreRate}. Back off when currentlyAvailable drops below your next query's cost. Standard shops = 100 points bucket, 50/s restore; Plus = 1000/100.Base curl pattern (reusable):
bashshop_gql() { local query="$1" local variables="${2:-{}}" curl -sS -X POST \ "https://${SHOPIFY_STORE_DOMAIN}/admin/api/${SHOPIFY_API_VERSION:-2026-01}/graphql.json" \ -H "Content-Type: application/json" \ -H "X-Shopify-Access-Token: ${SHOPIFY_ACCESS_TOKEN}" \ --data "$(jq -nc --arg q "$query" --argjson v "$variables" '{query: $q, variables: $v}')" }
Pipe through jq for readable output. -sS keeps errors visible but hides the progress bar.
bashshop_gql '{ shop { name myshopifyDomain primaryDomain { url } currencyCode plan { displayName } } }' | jq
bashshop_gql '{ publicApiVersions { handle supported } }' | jq '.data.publicApiVersions[] | select(.supported)'
bashshop_gql ' query($q: String!) { products(first: 20, query: $q) { edges { node { id title handle status totalInventory variants(first: 5) { edges { node { id sku price inventoryQuantity } } } } } pageInfo { hasNextPage endCursor } } }' '{"q":"hoodie status:active"}' | jq
Query syntax supports title:, sku:, vendor:, product_type:, status:active, tag:, created_at:>2025-01-01. Full grammar: https://shopify.dev/docs/api/usage/search-syntax
bashshop_gql ' query($cursor: String) { products(first: 100, after: $cursor) { edges { cursor node { id handle } } pageInfo { hasNextPage endCursor } } }' '{"cursor":null}' # subsequent calls: pass the previous endCursor
bashshop_gql ' query($id: ID!) { product(id: $id) { id title handle descriptionHtml tags status variants(first: 20) { edges { node { id sku price compareAtPrice inventoryQuantity selectedOptions { name value } } } } metafields(first: 20) { edges { node { namespace key type value } } } } }' '{"id":"gid://shopify/Product/10079467700516"}' | jq
bashshop_gql ' mutation($input: ProductCreateInput!) { productCreate(product: $input) { product { id handle } userErrors { field message } } }' '{"input":{"title":"Test Hoodie","status":"DRAFT","vendor":"Hermes","productType":"Apparel","tags":["test"]}}'
Variants now have their own mutations in recent versions:
bash# Add variants after creating the product shop_gql ' mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { productVariantsBulkCreate(productId: $productId, variants: $variants) { productVariants { id sku price } userErrors { field message } } }' '{"productId":"gid://shopify/Product/...","variants":[{"optionValues":[{"optionName":"Size","name":"M"}],"price":"49.00","inventoryItem":{"sku":"HD-M","tracked":true}}]}'
bashshop_gql ' mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { productVariantsBulkUpdate(productId: $productId, variants: $variants) { productVariants { id sku price } userErrors { field message } } }' '{"productId":"gid://shopify/Product/...","variants":[{"id":"gid://shopify/ProductVariant/...","price":"55.00"}]}'
read_all_orders)bashshop_gql ' { orders(first: 20, reverse: true, query: "financial_status:paid") { edges { node { id name createdAt displayFinancialStatus displayFulfillmentStatus totalPriceSet { shopMoney { amount currencyCode } } customer { id displayName email } lineItems(first: 10) { edges { node { title quantity sku } } } } } } }' | jq
Useful order query filters: financial_status:paid|pending|refunded, fulfillment_status:unfulfilled|fulfilled, created_at:>2025-01-01, tag:gift, email:foo@example.com.
bashshop_gql ' query($id: ID!) { order(id: $id) { id name email shippingAddress { name address1 address2 city province country zip phone } lineItems(first: 50) { edges { node { title quantity variant { sku } originalUnitPriceSet { shopMoney { amount currencyCode } } } } } transactions { id kind status amountSet { shopMoney { amount currencyCode } } } } }' '{"id":"gid://shopify/Order/...."}' | jq
bash# Search shop_gql ' { customers(first: 10, query: "email:*@example.com") { edges { node { id email displayName numberOfOrders amountSpent { amount currencyCode } } } } }' # Create shop_gql ' mutation($input: CustomerInput!) { customerCreate(input: $input) { customer { id email } userErrors { field message } } }' '{"input":{"email":"test@example.com","firstName":"Test","lastName":"User","tags":["api-created"]}}'
Inventory lives on inventory items tied to variants, quantities tracked per location.
bash# Get inventory for a variant across all locations shop_gql ' query($id: ID!) { productVariant(id: $id) { id sku inventoryItem { id tracked inventoryLevels(first: 10) { edges { node { location { id name } quantities(names: ["available","on_hand","committed"]) { name quantity } } } } } } }' '{"id":"gid://shopify/ProductVariant/..."}'
Adjust stock (delta) — uses inventoryAdjustQuantities:
bashshop_gql ' mutation($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/...", "locationId": "gid://shopify/Location/..."}] } }'
Set absolute stock (not delta) — inventorySetQuantities:
bashshop_gql ' mutation($input: InventorySetQuantitiesInput!) { inventorySetQuantities(input: $input) { inventoryAdjustmentGroup { id } userErrors { field message } } }' '{"input":{"reason":"correction","name":"available","ignoreCompareQuantity":true,"quantities":[{"inventoryItemId":"gid://shopify/InventoryItem/...","locationId":"gid://shopify/Location/...","quantity":100}]}}'
Metafields attach custom data to resources (products, customers, orders, shop).
bash# Read shop_gql ' query($id: ID!) { product(id: $id) { metafields(first: 10, namespace: "custom") { edges { node { key type value } } } } }' '{"id":"gid://shopify/Product/..."}' # Write (works for any owner type) shop_gql ' mutation($metafields: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $metafields) { metafields { id key namespace } userErrors { field message code } } }' '{"metafields":[{"ownerId":"gid://shopify/Product/...","namespace":"custom","key":"care_instructions","type":"multi_line_text_field","value":"Wash cold. Tumble dry low."}]}'
Different endpoint, different token, used for customer-facing apps/hydrogen-style headless setups. Headers differ:
https://$SHOPIFY_STORE_DOMAIN/api/$SHOPIFY_API_VERSION/graphql.jsonX-Shopify-Storefront-Access-Token: <public token> — embeddable in browserShopify-Storefront-Private-Token: <private token> — server-onlybashcurl -sS -X POST \ "https://${SHOPIFY_STORE_DOMAIN}/api/${SHOPIFY_API_VERSION:-2026-01}/graphql.json" \ -H "Content-Type: application/json" \ -H "X-Shopify-Storefront-Access-Token: ${SHOPIFY_STOREFRONT_TOKEN}" \ -d '{"query":"{ shop { name } products(first: 5) { edges { node { id title handle } } } }"}' | jq
For dumps larger than rate limits allow (full product catalog, all orders for a year):
bash# 1. Start bulk query shop_gql ' mutation { bulkOperationRunQuery(query: """ { products { edges { node { id title handle variants { edges { node { sku price } } } } } } } """) { bulkOperation { id status } userErrors { field message } } }' # 2. Poll status shop_gql '{ currentBulkOperation { id status errorCode objectCount fileSize url partialDataUrl } }' # 3. When status=COMPLETED, download the JSONL file curl -sS "$URL" > products.jsonl
Each JSONL line is a node, and nested connections are emitted as separate lines with __parentId. Reassemble client-side if needed.
Subscribe to events so you don't have to poll:
bashshop_gql ' mutation($topic: WebhookSubscriptionTopic!, $sub: WebhookSubscriptionInput!) { webhookSubscriptionCreate(topic: $topic, webhookSubscription: $sub) { webhookSubscription { id topic endpoint { __typename ... on WebhookHttpEndpoint { callbackUrl } } } userErrors { field message } } }' '{"topic":"ORDERS_CREATE","sub":{"callbackUrl":"https://example.com/webhook","format":"JSON"}}'
Verify incoming webhook HMAC using the app's client secret (not the access token):
bashecho -n "$REQUEST_BODY" | openssl dgst -sha256 -hmac "$APP_SECRET" -binary | base64 # Compare to X-Shopify-Hmac-Sha256 header
/admin/api/.../products.json. Use GraphQL.shpat_. Storefront public tokens with shpua_. If you have one and the wrong header, every request returns 401 without a useful error body.{"errors":[{"message":"Access denied for ..."}]}. Re-configure Admin API scopes on the app, then reinstall to regenerate the token.userErrors is empty != success. Also check data.<mutation>.<resource> is non-null. Some failures populate neither — inspect the whole response.gid://shopify/Product/<numeric>.products(first: 250) with deep nesting can cost 1000+ points and throttle immediately on a standard-plan shop. Start narrow, read extensions.cost, adjust.products(first: N, reverse: true) sorts by id DESC, not created_at. Use sortKey: CREATED_AT, reverse: true for "newest first."read_all_orders for historical data. Without it, orders(...) silently caps at the 60-day window. You won't get an error, just fewer results than expected. For Shopify Plus merchants with many orders, request this scope via the app's protected-data settings."49.00" not 49.0. Don't jq tonumber blindly if you care about zero-padding.shopMoney (store's currency) AND presentmentMoney (customer's). Pick one consistently.Mutations in Shopify are real — they create products, charge refunds, cancel orders, ship fulfillments. Before running productDelete, orderCancel, refundCreate, or any bulk mutation: state clearly what the change is, on which shop, and confirm with the user. There is no staging clone of production data unless the user has a separate dev store.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-22 | pass→pass | 9,623 | 7,698 | -20% | 1 | 1 | 0% | 1,958 | 5,420 | +177% | 0 | 0 | — |
case-01 | pass→pass | 5,575 | 3,011 | -46% | 1 | 1 | 0% | 1,070 | 4,548 | +325% | 0 | 0 | — |
case-02 | pass→pass | 10,489 | 5,978 | -43% | 1 | 1 | 0% | 1,793 | 5,098 | +184% | 0 | 0 | — |
case-03 | pass→pass | 8,822 | 3,843 | -56% | 1 | 1 | 0% | 1,456 | 4,776 | +228% | 0 | 0 | — |
case-04 | pass→pass | 6,048 | 5,224 | -14% | 1 | 1 | 0% | 1,156 | 5,072 | +339% | 0 | 0 | — |
case-05 | pass→pass | 8,126 | 5,234 | -36% | 1 | 1 | 0% | 1,436 | 5,078 | +254% | 0 | 0 | — |
case-06 | pass→pass | 10,341 | 7,365 | -29% | 1 | 1 | 0% | 1,829 | 5,474 | +199% | 0 | 0 | — |
case-07 | pass→pass | 7,214 | 5,886 | -18% | 1 | 1 | 0% | 1,217 | 5,035 | +314% | 0 | 0 | — |
case-08 | pass→pass | 6,665 | 4,204 | -37% | 1 | 1 | 0% | 1,290 | 4,891 | +279% | 0 | 0 | — |
case-09 | pass→pass | 8,529 | 5,311 | -38% | 1 | 1 | 0% | 1,617 | 5,000 | +209% | 0 | 0 | — |
case-10 | pass→pass | 9,852 | 4,812 | -51% | 1 | 1 | 0% | 1,938 | 5,069 | +162% | 0 | 0 | — |
case-11 | pass→pass | 8,309 | 5,808 | -30% | 1 | 1 | 0% | 1,507 | 5,261 | +249% | 0 | 0 | — |
case-12 | pass→pass | 8,972 | 4,977 | -45% | 1 | 1 | 0% | 1,712 | 5,132 | +200% | 0 | 0 | — |
case-13 | pass→pass | 8,820 | 7,906 | -10% | 1 | 1 | 0% | 1,796 | 5,748 | +220% | 0 | 0 | — |
case-14 | pass→pass | 7,400 | 3,341 | -55% | 1 | 1 | 0% | 1,183 | 4,592 | +288% | 0 | 0 | — |
case-15 | pass→pass | 5,564 | 3,685 | -34% | 1 | 1 | 0% | 1,015 | 4,733 | +366% | 0 | 0 | — |
case-16 | fail→pass | 11,659 | 4,698 | -60% | 1 | 1 | 0% | 2,244 | 4,969 | +121% | 0 | 0 | — |
case-17 | pass→pass | 5,467 | 3,624 | -34% | 1 | 1 | 0% | 1,092 | 4,669 | +328% | 0 | 0 | — |
case-18 | pass→pass | 9,961 | 4,660 | -53% | 1 | 1 | 0% | 1,778 | 4,876 | +174% | 0 | 0 | — |
case-19 | pass→pass | 8,729 | 6,343 | -27% | 1 | 1 | 0% | 1,560 | 5,095 | +227% | 0 | 0 | — |
case-20 | pass→pass | 4,867 | 4,017 | -17% | 1 | 1 | 0% | 988 | 4,727 | +378% | 0 | 0 | — |
case-21 | fail→fail | 5,440 | 2,787 | -49% | 1 | 1 | 0% | 1,018 | 4,449 | +337% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.