Shopify

Запросы к Shopify Admin/Storefront GraphQL API через curl.

Метаданные навыка

ИсточникОпциональный (устанавливается по запросу) — hermes skills install official/productivity/shopify
Путьoptional-skills/productivity/shopify
Версия1.0.0
Авторcommunity
ЛицензияMIT
Платформыlinux, macos, windows
ТегиShopify, E-commerce, Commerce, API, GraphQL
Связанные навыкиairtable, xurl

Справочник: полный SKILL.md

инфо

Ниже — полное определение навыка, которое Hermes загружает при его активации. Именно этот текст агент получает в виде инструкций, когда навык включён.

Shopify — Admin & Storefront GraphQL APIs

Работа с магазином Shopify напрямую через curl: просмотр товаров, управление складом, выгрузка заказов, обновление покупателей, чтение метаполей. Никаких SDK и фреймворков — только GraphQL-эндпоинт и токен кастомного приложения.

REST Admin API с апреля 2024 года считается устаревшим и получает только патчи безопасности. Для всей административной работы используй GraphQL Admin. Для публичных запросов на стороне покупателя (товары, коллекции, корзина) — Storefront GraphQL.

Требования

  1. В Shopify admin: Settings → Apps and sales channels → Develop apps → Create an app.
  2. Нажми Configure Admin API scopes, выбери нужные права (примеры ниже), сохрани.
  3. Install app → токен Admin API покажется ровно один раз. Сразу скопируй — Shopify больше его не покажет. Токены начинаются с shpat_.
  4. Запиши в ${HERMES_HOME:-~/.hermes}/.env:
    SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxx
    SHOPIFY_STORE_DOMAIN=my-store.myshopify.com
    SHOPIFY_API_VERSION=2026-01

Внимание: с 1 января 2026 года создание «legacy custom apps» через Shopify Admin недоступно. Новые приложения надо регистрировать через Dev Dashboard (shopify.dev/docs/apps/build/dev-dashboard). Уже созданные через Admin приложения продолжают работать. Если у пользователя нет существующего кастомного приложения и дата после 2026-01-01 — направь его в Dev Dashboard, а не в Admin.

Типичные права по задачам:

  • Товары / коллекции: read_products, write_products
  • Склад: read_inventory, write_inventory, read_locations
  • Заказы: read_orders, write_orders (30 последних без read_all_orders)
  • Покупатели: read_customers, write_customers
  • Черновики заказов: read_draft_orders, write_draft_orders
  • Фулфилмент: read_fulfillments, write_fulfillments
  • Метаполя / метаобъекты: покрываются соответствующими правами ресурса

Основы API

  • Эндпоинт: https://$SHOPIFY_STORE_DOMAIN/admin/api/$SHOPIFY_API_VERSION/graphql.json
  • Заголовок авторизации: X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN (НЕ Authorization: Bearer)
  • Метод: всегда POST, всегда Content-Type: application/json, тело — {"query": "...", "variables": {...}}
  • HTTP 200 не означает успех. GraphQL возвращает ошибки в массиве errors верхнего уровня и в userErrors у каждого поля. Проверяй оба.
  • ID — строки GID: gid://shopify/Product/10079467700516, gid://shopify/Variant/..., gid://shopify/Order/.... Передавай их целиком — префикс не обрезай.
  • Лимит запросов рассчитывается по стоимости запроса (алгоритм leaky bucket). Каждый ответ содержит extensions.cost с полями requestedQueryCost, actualQueryCost, throttleStatus.{currentlyAvailable, maximumAvailable, restoreRate}. Снижай нагрузку, когда currentlyAvailable падает ниже стоимости следующего запроса. Стандартный магазин — 100 очков, восстановление 50/с; Plus — 1000/100.

Базовый паттерн curl (многоразовый):

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}')"
}

Прогоняй через jq для читабельного вывода. -sS скрывает прогресс-бар, но оставляет ошибки.

Разведка API

Информация о магазине + текущая версия API

shop_gql '{ shop { name myshopifyDomain primaryDomain { url } currencyCode plan { displayName } } }' | jq

Список всех поддерживаемых версий API

shop_gql '{ publicApiVersions { handle supported } }' | jq '.data.publicApiVersions[] | select(.supported)'

Товары

Поиск товаров (первые 20 совпадений)

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"}' | jq

Синтаксис запроса поддерживает: title:, sku:, vendor:, product_type:, status:active, tag:, created_at:>2025-01-01. Полная грамматика: https://shopify.dev/docs/api/usage/search-syntax

Постраничная выгрузка товаров (по курсору)

shop_gql '
query($cursor: String) {
  products(first: 100, after: $cursor) {
    edges { cursor node { id handle } }
    pageInfo { hasNextPage endCursor }
  }
}' '{"cursor":null}'
# последующие вызовы: передавай предыдущий endCursor

Получить товар с вариантами и метаполями

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"}' | jq

Создать товар с одним вариантом

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"]}}'

В последних версиях варианты управляются отдельными мутациями:

# Добавить варианты после создания товара
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}}]}'

Обновить цену / 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"}]}'

Заказы

Список последних заказов (по умолчанию 30 без read_all_orders)

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 } } }
    } }
  }
}' | jq

Полезные фильтры для заказов: financial_status:paid|pending|refunded, fulfillment_status:unfulfilled|fulfilled, created_at:>2025-01-01, tag:gift, email:foo@example.com.

Получить конкретный заказ с адресом доставки

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/...."}' | jq

Покупатели

# Поиск
shop_gql '
{
  customers(first: 10, query: "email:*@example.com") {
    edges { node { id email displayName numberOfOrders amountSpent { amount currencyCode } } }
  }
}'

# Создание
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 items, привязанных к вариантам; количество отслеживается по location (складу/точке).

# Получить остатки варианта по всем складам
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/..."}'

Корректировка остатка (дельта) — через 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/..."}]
  }
}'

Установка абсолютного значения (не дельты) — 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}]}}'

Метаполя и метаобъекты

Метаполя добавляют произвольные данные к ресурсам: товарам, покупателям, заказам, магазину.

# Чтение
shop_gql '
query($id: ID!) {
  product(id: $id) {
    metafields(first: 10, namespace: "custom") {
      edges { node { key type value } }
    }
  }
}' '{"id":"gid://shopify/Product/..."}'

# Запись (работает для любого типа владельца)
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 (публичный, только чтение)

Отдельный эндпоинт, отдельный токен. Используется для клиентских приложений и headless-витрин (Hydrogen и т.п.). Заголовки отличаются:

  • Эндпоинт: https://$SHOPIFY_STORE_DOMAIN/api/$SHOPIFY_API_VERSION/graphql.json
  • Заголовок (публичный): X-Shopify-Storefront-Access-Token: <public token> — можно вставлять в браузерный код
  • Заголовок (приватный): Shopify-Storefront-Private-Token: <private token> — только для сервера
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 } } } }"}' | jq

Массовые операции

Для выгрузок, превышающих лимиты запросов (полный каталог товаров, заказы за год):

# 1. Запустить массовый запрос
shop_gql '
mutation {
  bulkOperationRunQuery(query: """
    { products { edges { node { id title handle variants { edges { node { sku price } } } } } } }
  """) {
    bulkOperation { id status }
    userErrors { field message }
  }
}'

# 2. Опрашивать статус
shop_gql '{ currentBulkOperation { id status errorCode objectCount fileSize url partialDataUrl } }'

# 3. Когда status=COMPLETED, скачать JSONL-файл
curl -sS "$URL" > products.jsonl

Каждая строка JSONL — отдельный узел; вложенные связи выводятся как отдельные строки с __parentId. При необходимости собирай иерархию на стороне клиента.

Вебхуки

Подписка на события избавляет от необходимости постоянно опрашивать API:

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"}}'

Проверка HMAC входящего вебхука — через client secret приложения (не через access token):

echo -n "$REQUEST_BODY" | openssl dgst -sha256 -hmac "$APP_SECRET" -binary | base64
# Сравни с заголовком X-Shopify-Hmac-Sha256

Подводные камни

  • REST-эндпоинты живы, но заморожены. Не пиши новые интеграции против /admin/api/.../products.json. Только GraphQL.
  • Формат токена. Admin-токены начинаются с shpat_. Публичные Storefront-токены — с shpua_. Если токен есть, но заголовок неверный, каждый запрос вернёт 401 без внятного тела ответа.
  • 403 с валидным токеном = нет нужного scope. Shopify вернёт {"errors":[{"message":"Access denied for ..."}]}. Перенастрой Admin API scopes в приложении, переустанови его — токен обновится.
  • Пустой userErrors ≠ успех. Проверяй также, что data.<mutation>.<resource> не null. Некоторые ошибки не попадают ни в одно из этих полей — смотри весь ответ целиком.
  • GID против числового ID. Старый REST возвращал числовые ID; GraphQL ожидает полные GID-строки. Конвертация: gid://shopify/Product/<numeric>.
  • Сюрпризы с лимитами. Один запрос products(first: 250) с глубокой вложенностью может стоить 1000+ очков и немедленно упереться в лимит на стандартном тарифе. Начинай с узких запросов, читай extensions.cost, корректируй.
  • Порядок пагинации. products(first: N, reverse: true) сортирует по id DESC, а не по created_at. Для «сначала новые» используй sortKey: CREATED_AT, reverse: true.
  • read_all_orders для исторических данных. Без этого scope orders(...) молча ограничивается окном в 60 дней. Ошибки не будет — просто меньше результатов, чем ожидаешь. Для Shopify Plus с большим объёмом заказов запрашивай этот scope через настройки защищённых данных приложения.
  • Суммы — строки. Amounts приходят как "49.00", а не 49.0. Не применяй jq tonumber вслепую, если важно нулевое выравнивание.
  • Поля Money с мультивалютностью содержат shopMoney (валюта магазина) и presentmentMoney (валюта покупателя). Выбери одно и придерживайся его.

Безопасность

Мутации в Shopify реальны — они создают товары, проводят возвраты, отменяют заказы, отмечают выполнение. Перед вызовом productDelete, orderCancel, refundCreate или любой массовой мутации чётко опиши, что именно изменится и в каком магазине, и получи подтверждение от пользователя. Staging-клона продакшн-данных нет, если только у пользователя не настроен отдельный dev store.

перевод и редактура:

обновлено

⌕ ESC