Shopify — Admin & Storefront GraphQL APIs (Bankr Edition)
Les sections Shopify de cette skill sont adaptées de la skill upstream
NousResearch/hermes-agent(MIT,author: community). Les quatre sections « Bankr Bridges » à la fin sont nouvelles et spécifiques à l'écosystème Bankr.
Travaillez directement avec les stores Shopify via curl : lister les produits, gérer l'inventaire, extraire les commandes, mettre à jour les clients, lire les metafields. Pas de SDK, pas de framework d'app — juste l'endpoint GraphQL et un token d'accès d'app personnalisée. Puis reliez les données marchands aux primitives onchain via Bankr.
L'API Admin REST est obsolète depuis 2024-04 et ne reçoit que les correctifs de sécurité. Utilisez GraphQL Admin pour tous les travaux d'administration. Utilisez Storefront GraphQL pour les requêtes en lecture seule côté client (produits, collections, panier).
Prérequis
- Dans l'admin Shopify : Paramètres → Applications et canaux de vente → Développer des applications → Créer une application.
- Cliquez sur Configurer les scopes de l'API Admin, sélectionnez ce dont vous avez besoin (exemples ci-dessous), enregistrez.
- Installez l'application → le token d'accès de l'API Admin apparaît UNE SEULE FOIS. Copiez-le immédiatement — Shopify ne le montrera plus jamais. Les tokens commencent par
shpat_. - Définissez ces variables dans vos paramètres Bankr (icône engrenage → Env Vars) :
SHOPIFY_ACCESS_TOKEN— token admin (commence parshpat_)SHOPIFY_STORE_DOMAIN—my-store.myshopify.com(le domaine myshopify permanent, pas votre domaine personnalisé)SHOPIFY_API_VERSION— par défaut2026-01BANKR_API_KEY— pour les sections Bankr bridge ; générez-le sur https://bankr.bot/api
Attention : À partir du 1er janvier 2026, les nouvelles « apps personnalisées héritées » créées dans l'admin Shopify disparaissent. Les nouvelles installations devraient utiliser le Dev Dashboard (
shopify.dev/docs/apps/build/dev-dashboard). Les apps admin existantes continuent à fonctionner. Si le store de l'utilisateur n'a pas d'app personnalisée existante et que c'est après le 2026-01-01, dirigez-le vers le Dev Dashboard au lieu du flux admin.
Scopes courants par tâche :
- Produits / collections :
read_products,write_products - Inventaire :
read_inventory,write_inventory,read_locations - Commandes :
read_orders,write_orders(30 plus récentes sansread_all_orders) - Clients :
read_customers,write_customers - Brouillons de commande :
read_draft_orders,write_draft_orders - Expéditions :
read_fulfillments,write_fulfillments - Metafields / metaobjects : couverts par les scopes des ressources correspondantes
Bases de l'API
- Endpoint :
https://$SHOPIFY_STORE_DOMAIN/admin/api/$SHOPIFY_API_VERSION/graphql.json - En-tête d'auth :
X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN(PASAuthorization: Bearer) - Méthode : toujours
POST, toujoursContent-Type: application/json, le corps est{"query": "...", "variables": {...}} - HTTP 200 ne signifie pas succès. GraphQL retourne les erreurs dans un tableau
errorsde haut niveau et desuserErrorspar champ. Vérifiez toujours les deux. - Les IDs sont des chaînes GID :
gid://shopify/Product/10079467700516,gid://shopify/Variant/...,gid://shopify/Order/.... Passez-les tels quels — ne supprimez pas le préfixe. - Rate limit : calculé via le coût de la requête (leaky bucket). Chaque réponse a
extensions.costavecrequestedQueryCost,actualQueryCost,throttleStatus.{currentlyAvailable, maximumAvailable, restoreRate}. Reculez quandcurrentlyAvailabletombe en dessous du coût de votre prochaine requête. Shops standard = bucket de 100 points, restauration 50/s ; Plus = 1000/100.
Motif curl de base (réutilisable) :
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}')"
}
Canalisez à travers jq pour une sortie lisible. -sS garde les erreurs visibles mais masque la barre de progression.
Découverte
Info du shop + version actuelle de l'API
shop_gql '{ shop { name myshopifyDomain primaryDomain { url } currencyCode plan { displayName } } }' | jq
Lister toutes les versions d'API supportées
shop_gql '{ publicApiVersions { handle supported } }' | jq '.data.publicApiVersions[] | select(.supported)'
Produits
Rechercher des produits (20 premiers correspondant à la requête)
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
La syntaxe de requête supporte title:, sku:, vendor:, product_type:, status:active, tag:, created_at:>2025-01-01. Grammaire complète : https://shopify.dev/docs/api/usage/search-syntax
Paginer les produits (cursor)
shop_gql '
query($cursor: String) {
products(first: 100, after: $cursor) {
edges { cursor node { id handle } }
pageInfo { hasNextPage endCursor }
}
}' '{"cursor":null}'
# appels suivants : passez le endCursor précédent
Obtenir un produit avec variantes + 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"}' | jq
Créer un produit avec une variante
shop_gql '
mutation($input: ProductCreateInput!) {
productCreate(product: $input) {
product { id handle }
userErrors { field message }
}
}' '{"input":{"title":"Test Hoodie","status":"DRAFT","vendor":"Bankr","productType":"Apparel","tags":["test"]}}'
Les variantes ont maintenant leurs propres mutations dans les versions récentes :
# Ajouter des variantes après la création du produit
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}}]}'
Mettre à jour le prix / 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"}]}'
Commandes
Lister les commandes récentes (30 dernières par défaut sans 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
Filtres de requête utiles pour les commandes : financial_status:paid|pending|refunded, fulfillment_status:unfulfilled|fulfilled, created_at:>2025-01-01, tag:gift, email:foo@example.com.
Récupérer une commande unique avec adresse d'expédition
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
Clients
# Rechercher
shop_gql '
{
customers(first: 10, query: "email:*@example.com") {
edges { node { id email displayName numberOfOrders amountSpent { amount currencyCode } } }
}
}'
# Créer
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"]}}'
Inventaire
L'inventaire réside sur les éléments d'inventaire liés aux variantes, avec les quantités suivies par localisation.
# Obtenir l'inventaire d'une variante dans toutes les localisations
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/..."}'
Ajuster le stock (delta) — utilise 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/..."}]
}
}'
Définir un stock absolu (pas un 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
Les metafields attachent des données personnalisées aux ressources (produits, clients, commandes, shop).
# Lire
shop_gql '
query($id: ID!) {
product(id: $id) {
metafields(first: 10, namespace: "custom") {
edges { node { key type value } }
}
}
}' '{"id":"gid://shopify/Product/..."}'
# Écrire (fonctionne pour tout type de propriétaire)
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 (lecture publique uniquement)
Endpoint différent, token différent, utilisé pour les apps côté client / configurations headless style hydrogen. Les en-têtes diffèrent :
- Endpoint :
https://$SHOPIFY_STORE_DOMAIN/api/$SHOPIFY_API_VERSION/graphql.json - En-tête d'auth (publique) :
X-Shopify-Storefront-Access-Token: <public token>— intégrable dans le navigateur - En-tête d'auth (privée) :
Shopify-Storefront-Private-Token: <private token>— serveur uniquement
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
Opérations en bloc
Pour les dumps plus grands que ce que les rate limits permettent (catalogue complet de produits, toutes les commandes d'une année) :
# 1. Démarrer la requête en bloc
shop_gql '
mutation {
bulkOperationRunQuery(query: """
{ products { edges { node { id title handle variants { edges { node { sku price } } } } } } }
""") {
bulkOperation { id status }
userErrors { field message }
}
}'
# 2. Interroger le statut
shop_gql '{ currentBulkOperation { id status errorCode objectCount fileSize url partialDataUrl } }'
# 3. Quand status=COMPLETED, téléchargez le fichier JSONL
curl -sS "$URL" > products.jsonl
Chaque ligne JSONL est un nœud, et les connexions imbriquées sont émises en tant que lignes séparées avec __parentId. Réassemblez côté client si nécessaire.
Webhooks
Abonnez-vous aux événements pour ne pas avoir à interroger :
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"}}'
Vérifiez le HMAC du webhook entrant avec le secret client de l'app (pas le token d'accès) :
echo -n "$REQUEST_BODY" | openssl dgst -sha256 -hmac "$APP_SECRET" -binary | base64
# Comparez à l'en-tête X-Shopify-Hmac-Sha256
Pièges
- Les endpoints REST existent toujours mais sont gelés. N'écrivez pas de nouvelles intégrations contre
/admin/api/.../products.json. Utilisez GraphQL. - Vérification du format du token. Les tokens admin commencent par
shpat_. Les tokens publics Storefront parshpua_. Si vous en avez un et le mauvais en-tête, chaque requête retourne 401 sans corps d'erreur utile. - 403 avec un token valide = scope manquant. Shopify retourne
{"errors":[{"message":"Access denied for ..."}]}. Reconfigurez les scopes de l'API Admin sur l'app, puis réinstallez pour régénérer le token. userErrorsvide ≠ succès. Vérifiez aussi quedata.<mutation>.<resource>n'est pas null. Certains échecs ne remplissent ni l'un ni l'autre — inspectez la réponse entière.- GID vs ID numérique. Le REST hérité donnait les IDs numériques ; GraphQL veut les chaînes GID complètes. Pour convertir :
gid://shopify/Product/<numeric>. - Surprise de rate limit. Une seule
products(first: 250)avec imbrication profonde peut coûter 1000+ points et throttle immédiatement sur un shop de plan standard. Commencez étroit, lisezextensions.cost, ajustez. - Ordre de pagination.
products(first: N, reverse: true)trie parid DESC, pascreated_at. UtilisezsortKey: CREATED_AT, reverse: truepour « plus récent d'abord ». read_all_orderspour les données historiques. Sans elle,orders(...)plafonne silencieusement à la fenêtre de 60 jours. Vous n'obtenez pas d'erreur, juste moins de résultats que prévu. Pour les marchands Shopify Plus avec de nombreuses commandes, demandez ce scope via les paramètres de données protégées de l'app.- Les devises sont des chaînes. Les montants reviennent sous forme
"49.00"et non49.0. Ne faites pasjq tonumberà l'aveugle si vous vous souciez du remplissage par zéro. - Les champs Money multi-devises ont
shopMoney(devise du store) ETpresentmentMoney(devise du client). Choisissez-en un de manière cohérente.
Sécurité
Les mutations dans Shopify sont réelles — elles créent des produits, facturent des remboursements, annulent les commandes, expédient les expéditions. Avant d'exécuter productDelete, orderCancel, refundCreate, ou toute mutation en bloc : déclarez clairement ce qu'est le changement, sur quel shop, et confirmez avec l'utilisateur. Il n'y a pas de clone intermédiaire des données de production à moins que l'utilisateur n'ait un store de développement séparé.
Bankr Bridges
Les sections ci-dessous ne font pas partie de la skill Hermes upstream. Elles montrent comment connecter les ressources Shopify aux primitives onchain de Bankr — résolution de handles, règlement x402, et flux de tokens pilotés par webhook — en utilisant uniquement ce que Bankr expose déjà.
D'après la documentation Bankr :
- L'agent de Bankr résout nativement ENS, Twitter @handle, Farcaster handle, et les adresses de portefeuille pour les transferts et les destinataires de frais. L'email ne peut pas être résolu.
- Bankr peut appeler, héberger et régler les endpoints x402 ; USDC est l'unité.
- Le motif Job est soumettre → interroger :
POST /agent/promptpuisGET /agent/job/{id}. Il n'y a pas de webhooks côté Bankr. - Outils MCP :
bankr_agent_submit_prompt,bankr_agent_get_job_status,bankr_agent_cancel_job.
Bridge 1 — Identité (Client Shopify ↔ Bankr handle)
La clé cliente principale de Shopify est l'email, que Bankr ne peut pas résoudre. Le bridge : stocker un handle résolvable par Bankr sur le client en tant que metafield, puis passer la chaîne telle quelle à Bankr.
Définition de metafield recommandée : namespace: custom, key: handle, type: single_line_text_field. Valeurs acceptables : vitalik.eth, @dwr.eth, @username (Twitter), ou une adresse portefeuille 0x….
# Écrire un handle sur un client à la caisse / inscription
shop_gql '
mutation($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields { id key value }
userErrors { field message code }
}
}' '{"metafields":[{"ownerId":"gid://shopify/Customer/123","namespace":"custom","key":"handle","type":"single_line_text_field","value":"vitalik.eth"}]}'
Relisez-le quand vous devez payer ou déposer des tokens :
HANDLE=$(shop_gql '
query($id: ID!) {
customer(id: $id) { metafield(namespace:"custom", key:"handle") { value } }
}' '{"id":"gid://shopify/Customer/123"}' | jq -r '.data.customer.metafield.value')
# Transférez à Bankr — aucune résolution supplémentaire nécessaire.
curl -sS -X POST https://api.bankr.bot/agent/prompt \
-H "Authorization: Bearer ${BANKR_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"prompt\":\"send 10 USDC to ${HANDLE} on base\"}"
Si le metafield est vide, demandez au client un parmi {ENS, Twitter, Farcaster, portefeuille} avant toute action onchain — ne devinez jamais à partir de l'email.
Bridge 2 — Checkout x402 pour un brouillon de commande Shopify
Bankr règle nativement les endpoints x402. Pour accepter l'USDC pour un panier Shopify, créez un brouillon de commande, exposez son total derrière un endpoint tarifié en x402, et laissez Bankr le payer. Au 200, marquez le brouillon comme payé (ou appelez draftOrderComplete).
# 1. Créer un brouillon de commande à partir d'un panier
DRAFT=$(shop_gql '
mutation($input: DraftOrderInput!) {
draftOrderCreate(input: $input) {
draftOrder { id totalPriceSet { shopMoney { amount currencyCode } } invoiceUrl }
userErrors { field message }
}
}' '{"input":{"lineItems":[{"variantId":"gid://shopify/ProductVariant/...","quantity":1}],"email":"buyer@example.com"}}')
DRAFT_ID=$(echo "$DRAFT" | jq -r '.data.draftOrderCreate.draftOrder.id')
TOTAL=$(echo "$DRAFT" | jq -r '.data.draftOrderCreate.draftOrder.totalPriceSet.shopMoney.amount')
Votre service expose ensuite un endpoint HTTP 402 tarifant ce brouillon (USDC sur Base) — voir la spec x402 upstream. Côté Bankr, l'agent le règle :
# Côté Bankr (agent) : payer l'endpoint x402 qui fronts le brouillon
curl -sS -X POST https://api.bankr.bot/agent/prompt \
-H "Authorization: Bearer ${BANKR_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"prompt\":\"call x402 endpoint https://shop.example.com/x402/draft/${DRAFT_ID} and settle in USDC on base\"}"
Quand votre serveur x402 confirme le règlement, complétez le brouillon côté Shopify :
shop_gql '
mutation($id: ID!) {
draftOrderComplete(id: $id, paymentPending: false) {
draftOrder { order { id name } }
userErrors { field message }
}
}' "{\"id\":\"${DRAFT_ID}\"}"
Notes :
- Utilisez
shopMoneyde manière cohérente pour la tarification x402 afin que la conversion de devises reste de votre côté, pas celui de Shopify. - Traitez le hash tx du règlement x402 comme source de vérité — ne complétez pas le brouillon jusqu'à ce que vous ayez vérifié le hash.
Bridge 3 — Webhook Shopify → Bankr Submit (drop de loyauté sur ORDERS_PAID)
Bankr n'a pas de webhooks entrants. Le motif est : webhook Shopify → votre serveur → vérifier HMAC → lire le metafield handle du client → soumettre un job Bankr → interroger.
# S'abonner une fois
shop_gql '
mutation($topic: WebhookSubscriptionTopic!, $sub: WebhookSubscriptionInput!) {
webhookSubscriptionCreate(topic: $topic, webhookSubscription: $sub) {
webhookSubscription { id topic }
userErrors { field message }
}
}' '{"topic":"ORDERS_PAID","sub":{"callbackUrl":"https://your.server/shopify/orders-paid","format":"JSON"}}'
Pseudocode du gestionnaire webhook :
# 1. Vérifier le HMAC (rejeter s'il ne correspond pas) — utilisez une comparaison en temps constant dans le code réel
EXPECTED=$(echo -n "$REQUEST_BODY" | openssl dgst -sha256 -hmac "$APP_SECRET" -binary | base64)
[ "$EXPECTED" = "$X_SHOPIFY_HMAC_SHA256" ] || exit 1
# 2. Extraire le handle Bankr de l'acheteur du metafield client de la commande
CUSTOMER_ID=$(echo "$REQUEST_BODY" | jq -r '.customer.admin_graphql_api_id')
HANDLE=$(shop_gql '
query($id: ID!) {
customer(id: $id) { metafield(namespace:"custom", key:"handle") { value } }
}' "{\"id\":\"${CUSTOMER_ID}\"}" | jq -r '.data.customer.metafield.value')
[ -z "$HANDLE" ] || [ "$HANDLE" = "null" ] && exit 0 # ignorer silencieusement, pas de handle en dossier
# 3. Soumettre le drop de loyauté à Bankr
JOB=$(curl -sS -X POST https://api.bankr.bot/agent/prompt \
-H "Authorization: Bearer ${BANKR_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"prompt\":\"send 100 LOYALTY to ${HANDLE} on base\"}" | jq -r '.job_id')
# 4. Interroger jusqu'à terminal
while :; do
STATUS=$(curl -sS "https://api.bankr.bot/agent/job/${JOB}" \
-H "Authorization: Bearer ${BANKR_API_KEY}" | jq -r '.status')
case "$STATUS" in
completed|failed|cancelled) break ;;
esac
sleep 2
done
Même motif fonctionne pour : splits de royalties sur les biens numériques, airdrops de tokens Clanker aux acheteurs réguliers, sweeps de trésorerie quand un seuil de ventes est atteint. Ajoutez une clé d'idempotence sur l'id de commande Shopify afin qu'un webhook rejoué ne puisse pas déclencher une soumission Bankr dupliquée.
Bridge 4 — Antisèche de scopes pour les flows Bankr
Scopes Admin API minimaux par bridge :
| Flow | Scopes requis |
|---|---|
| Bridge d'identité (lire/écrire handle metafield) | read_customers, write_customers |
| Checkout x402 via brouillons de commande | read_customers, write_draft_orders, read_orders |
| Webhook → Bankr loyalty drop | read_orders, read_customers, plus abonnement webhook sur ORDERS_PAID |
| Export en bloc pour une réconciliation hors ligne contre les hash tx onchain | read_products, read_orders, read_customers |
Le token n'a besoin que de ce que l'agent utilisera réellement. Ne concédez pas les scopes write_* à un agent en lecture seule pour la réconciliation.
Crédit
Le contenu Shopify de base (tout ce qui précède l'en-tête « Bankr Bridges ») est adapté, avec attribution, de NousResearch/hermes-agent (optional-skills/productivity/shopify/SKILL.md), sous licence MIT. Les sections Bankr bridge sont nouvelles et contribuées sous la même licence MIT.