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.
Требования
- В Shopify admin: Settings → Apps and sales channels → Develop apps → Create an app.
- Нажми Configure Admin API scopes, выбери нужные права (примеры ниже), сохрани.
- Install app → токен Admin API покажется ровно один раз. Сразу скопируй — Shopify больше его не покажет. Токены начинаются с
shpat_. - Запиши в
${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для исторических данных. Без этого scopeorders(...)молча ограничивается окном в 60 дней. Ошибки не будет — просто меньше результатов, чем ожидаешь. Для Shopify Plus с большим объёмом заказов запрашивай этот scope через настройки защищённых данных приложения.- Суммы — строки. Amounts приходят как
"49.00", а не49.0. Не применяйjq tonumberвслепую, если важно нулевое выравнивание. - Поля Money с мультивалютностью содержат
shopMoney(валюта магазина) иpresentmentMoney(валюта покупателя). Выбери одно и придерживайся его.
Безопасность
Мутации в Shopify реальны — они создают товары, проводят возвраты, отменяют заказы, отмечают выполнение. Перед вызовом productDelete, orderCancel, refundCreate или любой массовой мутации чётко опиши, что именно изменится и в каком магазине, и получи подтверждение от пользователя. Staging-клона продакшн-данных нет, если только у пользователя не настроен отдельный dev store.