This is a working reference of copy-paste Shopify Admin GraphQL snippets for product catalog work. For the conceptual overview of how these APIs fit together — Product objects, productSet, the Product Feed API, and the Catalog API — start with the Shopify Product Catalog API guide.
Every snippet targets the GraphQL Admin API. Pin an explicit version in your endpoint (e.g. /admin/api/2025-07/graphql.json) and request only the fields you need — GraphQL bills by calculated query cost.
Reads
Get a single product by handle
query ProductByHandle($handle: String!) {
productByIdentifier(identifier: { handle: $handle }) {
id
title
descriptionHtml
status
variants(first: 100) { nodes { id sku barcode price } }
}
}
Paginate the full product list
query Products($cursor: String) {
products(first: 100, after: $cursor, sortKey: UPDATED_AT) {
pageInfo { hasNextPage endCursor }
nodes { id title handle status updatedAt }
}
}
Filter with a search query (status, type, tag)
query ActiveHats($cursor: String) {
products(first: 50, after: $cursor, query: "status:active AND product_type:Hats") {
pageInfo { hasNextPage endCursor }
nodes { id title productType }
}
}
Products in a collection
query CollectionProducts($id: ID!, $cursor: String) {
collection(id: $id) {
products(first: 100, after: $cursor) {
pageInfo { hasNextPage endCursor }
nodes { id title }
}
}
}
Variants low on inventory (feed hygiene)
query LowStock {
productVariants(first: 100, query: "inventory_quantity:<5") {
nodes { id sku displayName inventoryQuantity product { title } }
}
}
Pull every feed-critical field for one product
query FeedFields($id: ID!) {
product(id: $id) {
id title descriptionHtml productType vendor tags
featuredMedia { ... on MediaImage { image { url altText } } }
variants(first: 100) {
nodes { sku barcode price compareAtPrice inventoryQuantity selectedOptions { name value } }
}
metafields(first: 20) { nodes { namespace key value } }
}
}
Writes
Create or update a product declaratively (productSet)
mutation UpsertProduct($input: ProductSetInput!) {
productSet(input: $input) {
product { id title }
userErrors { field message }
}
}
Patch a single field (productUpdate)
mutation Retitle($id: ID!, $title: String!) {
productUpdate(product: { id: $id, title: $title }) {
product { id title }
userErrors { field message }
}
}
Bulk-update variant prices
mutation Reprice($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants { id price }
userErrors { field message }
}
}
Attach an image to a product
mutation AddImage($productId: ID!, $media: [CreateMediaInput!]!) {
productCreateMedia(productId: $productId, media: $media) {
media { ... on MediaImage { id image { url } } }
mediaUserErrors { field message }
}
}
Set a metafield on a product
mutation SetMetafield($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields { id namespace key value }
userErrors { field message }
}
}
Bulk export the whole catalog
Start a bulk query
mutation BulkExport {
bulkOperationRunQuery(
query: """
{ products { edges { node { id title
variants { edges { node { id sku barcode price inventoryQuantity } } } } } }
"""
) {
bulkOperation { id status }
userErrors { field message }
}
}
Poll for completion, then download
query BulkStatus {
currentBulkOperation {
id
status
objectCount
url
}
}
When status is COMPLETED, stream the JSONL file at url. Each line is one object; child nodes (variants) reference their parent by __parentId.
Product feeds (channel distribution)
List existing feeds
query Feeds {
productFeeds(first: 20) {
nodes { id status country language }
}
}
Create a feed and trigger a full sync
mutation CreateFeed($input: ProductFeedInput!) {
productFeedCreate(input: $input) {
productFeed { id status }
userErrors { field message }
}
}
mutation FullSync($id: ID!) {
productFullSync(id: $id) {
userErrors { field message }
}
}
Catalogs and price lists (B2B / Markets)
List company-location catalogs
query Catalogs {
catalogs(first: 20, type: COMPANY_LOCATION) {
nodes { title status priceList { name currency } }
}
}
Read a price list's fixed prices
query PriceListPrices($id: ID!) {
priceList(id: $id) {
name
currency
prices(first: 100) {
nodes { variant { sku } price { amount currencyCode } }
}
}
}
Publish a product to a sales channel
mutation Publish($id: ID!, $input: [PublicationInput!]!) {
publishablePublish(id: $id, input: $input) {
publishable { availablePublicationsCount { count } }
userErrors { field message }
}
}
Webhooks (keep feeds fresh)
Subscribe to product updates
mutation SubscribeProductUpdate($callbackUrl: URL!) {
webhookSubscriptionCreate(
topic: PRODUCTS_UPDATE
webhookSubscription: { callbackUrl: $callbackUrl, format: JSON }
) {
webhookSubscription { id topic }
userErrors { field message }
}
}
Subscribe to PRODUCTS_CREATE, PRODUCTS_UPDATE, PRODUCTS_DELETE, and INVENTORY_LEVELS_UPDATE the same way. Process events idempotently and debounce bursts.
Handling rate limits
The GraphQL Admin API uses a calculated, cost-based limit. Every response includes extensions.cost with requestedQueryCost, actualQueryCost, and a throttleStatus bucket. Read it and back off when currentlyAvailable runs low:
{
"extensions": {
"cost": {
"requestedQueryCost": 112,
"actualQueryCost": 51,
"throttleStatus": { "maximumAvailable": 2000, "currentlyAvailable": 1949, "restoreRate": 100 }
}
}
}
For anything that would burn a large share of the bucket, use Bulk Operations instead. For OAuth, scopes, and the deeper rate-limit model, see the Shopify Admin API guide; for the strategy of turning this catalog data into ad and AI-shopping performance, see the Shopify Product Catalog API guide.