Use it when your goal in payments and business operations is clear and you can limit it to the data and actions it actually needs.
Shopify
Query Shopify Admin/Storefront GraphQL APIs via curl
What does it add to Hermes?
Query Shopify Admin/Storefront GraphQL APIs via curl
Shopify is a skill related to payments and business operations. It handles financial or commercial data and actions; any write operation needs stronger approval and limits than ordinary use.
This plain-language explanation is based on the publisher description. The original text remains visible for verification.
Do not add it merely to experiment when Hermes already has a simpler path, or when you cannot review its source and permissions.
Best for users who want a repeatable way of working inside Hermes.
Use a sandbox or test data and do not grant real payment or refund permissions in the first test.
Query Shopify Admin/Storefront GraphQL APIs via curl
This entry was indexed from Hermes Optional Skills. Our explanation interprets the type and domain without inventing a capability not present upstream.
The source is official or editorially reviewed, but you still need to review permissions and version compatibility.
Inspect, install, then test.
- 01Open the source
Match the publisher, license, and description to your need. Check the real update history.
- 02Review permissions and secrets
Never paste a secret value into this site. Use environment-variable names and grant the smallest scope.
- 03Copy setup only after review
The controls below copy text. They do not execute commands on your device.
- 04Test with a non-sensitive task
Inspect the visible tools, then exclude write or delete tools you do not need.
Review the command, then copy it.
hermes skills install shopifyHermes Belarabi does not execute this command. Installation happens on your device and remains subject to Hermes scanning and your review.
Exactly what Hermes loads when this skill runs.
Reproduced from the official documentation. Read it before enabling the skill: this text becomes the agent's instructions.
Query Shopify Admin/Storefront GraphQL APIs via curl.
Skill metadata
A lookup table. Do not read it all; find the row that applies to you.
Reference: full SKILL.md
Explains the idea itself. Read it slowly; the later sections build on it.
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).
Prerequisites
Settings you configure once. Change one at a time so you can see what each does. Set SHOPIFY_ACCESS_TOKEN, SHOPIFY_STORE_DOMAIN in your environment, not in the chat.
- In Shopify admin: Settings → Apps and sales channels → Develop apps → Create an app.
- Click Configure Admin API scopes, select what you need (examples below), save.
- Install app → the Admin API access token appears ONCE. Copy it immediately — Shopify will never show it again. Tokens start with
shpat_. - Save to
${HERMES_HOME:-~/.hermes}/.env:
SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxx
SHOPIFY_STORE_DOMAIN=my-store.myshopify.com
SHOPIFY_API_VERSION=2026-01Heads 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:
- Products / collections:
read_products,write_products - Inventory:
read_inventory,write_inventory,read_locations - Orders:
read_orders,write_orders(30 most recent withoutread_all_orders) - Customers:
read_customers,write_customers - Draft orders:
read_draft_orders,write_draft_orders - Fulfillments:
read_fulfillments,write_fulfillments - Metafields / metaobjects: covered by the matching resource scopes
API Basics
Settings you configure once. Change one at a time so you can see what each does. Set SHOPIFY_STORE_DOMAIN, SHOPIFY_API_VERSION in your environment, not in the chat.
- Endpoint:
https://$SHOPIFY_STORE_DOMAIN/admin/api/$SHOPIFY_API_VERSION/graphql.json - Auth header:
X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN(NOTAuthorization: Bearer) - Method: always
POST, alwaysContent-Type: application/json, body is{"query": "...", "variables": {...}} - HTTP 200 does not mean success. GraphQL returns errors in a top-level
errorsarray and per-fielduserErrors. Always check both. - IDs are GID strings:
gid://shopify/Product/10079467700516,gid://shopify/Variant/...,gid://shopify/Order/.... Pass these verbatim — don't strip the prefix. - Rate limit: calculated via query cost (leaky bucket). Each response has
extensions.costwithrequestedQueryCost,actualQueryCost,throttleStatus.{currentlyAvailable, maximumAvailable, restoreRate}. Back off whencurrentlyAvailabledrops below your next query's cost. Standard shops = 100 points bucket, 50/s restore; Plus = 1000/100.
Base curl pattern (reusable):
shop_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.
Discovery
Explains the idea itself. Read it slowly; the later sections build on it.
Shop info + current API version
shop_gql '{ shop { name myshopifyDomain primaryDomain { url } currencyCode plan { displayName } } }' | jqList all supported API versions
shop_gql '{ publicApiVersions { handle supported } }' | jq '.data.publicApiVersions[] | select(.supported)'Products
Explains the idea itself. Read it slowly; the later sections build on it.
Search products (first 20 matching query)
shop_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"}' | jqQuery 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
Paginate products (cursor)
shop_gql '
query($cursor: String) {
products(first: 100, after: $cursor) {
edges { cursor node { id handle } }
pageInfo { hasNextPage endCursor }
}
}' '{"cursor":null}'
# subsequent calls: pass the previous endCursorGet a product with variants + metafields
shop_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"}' | jqCreate a product with one variant
shop_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:
# 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}}]}'Update price / SKU
shop_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"}]}'Orders
Explains the idea itself. Read it slowly; the later sections build on it.
List recent orders (last 30 by default without readallorders)
shop_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 } } }
} }
}
}' | jqUseful order query filters: financial_status:paid|pending|refunded, fulfillment_status:unfulfilled|fulfilled, created_at:>2025-01-01, tag:gift, email:foo@example.com.
Fetch a single order with shipping address
shop_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/...."}' | jqCustomers
Explains the idea itself. Read it slowly; the later sections build on it.
# 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
Explains the idea itself. Read it slowly; the later sections build on it.
Inventory lives on inventory items tied to variants, quantities tracked per location.
# 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:
shop_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:
shop_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 & Metaobjects
Explains the idea itself. Read it slowly; the later sections build on it.
Metafields attach custom data to resources (products, customers, orders, shop).
# 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."}]}'Storefront API (public read-only)
Settings you configure once. Change one at a time so you can see what each does. Set SHOPIFY_STORE_DOMAIN, SHOPIFY_API_VERSION in your environment, not in the chat.
Different endpoint, different token, used for customer-facing apps/hydrogen-style headless setups. Headers differ:
- Endpoint:
https://$SHOPIFY_STORE_DOMAIN/api/$SHOPIFY_API_VERSION/graphql.json - Auth header (public):
X-Shopify-Storefront-Access-Token: <public token>— embeddable in browser - Auth header (private):
Shopify-Storefront-Private-Token: <private token>— server-only
curl -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 } } } }"}' | jqBulk Operations
Explains the idea itself. Read it slowly; the later sections build on it.
For dumps larger than rate limits allow (full product catalog, all orders for a year):
# 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.jsonlEach JSONL line is a node, and nested connections are emitted as separate lines with __parentId. Reassemble client-side if needed.
Webhooks
Settings you configure once. Change one at a time so you can see what each does. Set ORDERS_CREATE, REQUEST_BODY in your environment, not in the chat.
Subscribe to events so you don't have to poll:
shop_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):
echo -n "$REQUEST_BODY" | openssl dgst -sha256 -hmac "$APP_SECRET" -binary | base64
# Compare to X-Shopify-Hmac-Sha256 headerPitfalls
Explains the idea itself. Read it slowly; the later sections build on it.
- REST endpoints still exist but are frozen. Don't write new integrations against
/admin/api/.../products.json. Use GraphQL. - Token format check. Admin tokens start with
shpat_. Storefront public tokens withshpua_. If you have one and the wrong header, every request returns 401 without a useful error body. - 403 with a valid token = missing scope. Shopify returns
{"errors":[{"message":"Access denied for ..."}]}. Re-configure Admin API scopes on the app, then reinstall to regenerate the token. userErrorsis empty != success. Also checkdata.<mutation>.<resource>is non-null. Some failures populate neither — inspect the whole response.- GID vs numeric ID. Legacy REST gave numeric IDs; GraphQL wants full GID strings. To convert:
gid://shopify/Product/<numeric>. - Rate limit surprise. A single
products(first: 250)with deep nesting can cost 1000+ points and throttle immediately on a standard-plan shop. Start narrow, readextensions.cost, adjust. - Pagination order.
products(first: N, reverse: true)sorts byid DESC, notcreated_at. UsesortKey: CREATED_AT, reverse: truefor "newest first." read_all_ordersfor 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.- Currencies are strings. Amounts come back as
"49.00"not49.0. Don'tjq tonumberblindly if you care about zero-padding. - Multi-currency Money fields have
shopMoney(store's currency) ANDpresentmentMoney(customer's). Pick one consistently.
Safety
Explains the idea itself. Read it slowly; the later sections build on it.
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.