REST is deprecated — use GraphQL
Shopify announced REST Product endpoints deprecated for new apps on February 1, 2025. All new option-management integrations must use the Admin GraphQL API. Existing REST integrations continue to work but won't receive new features (including the 2,048-variant cap, which requires GraphQL to fully utilize). If you're building or maintaining a personalization app in 2026, use GraphQL exclusively.
Authentication + endpoint
Endpoint: https://{shop}.myshopify.com/admin/api/2025-07/graphql.json
Headers:
X-Shopify-Access-Token: {your app's access token}
Content-Type: application/json
Custom apps: generate the token in the admin (Settings → Apps → Develop apps → API credentials). Public apps: tokens come from the OAuth flow. Required scopes for personalization: read_products, write_products, read_orders, write_orders, read_inventory, write_inventory, read_metaobjects, write_metaobjects.
Query product options + variants
query ProductOptions($id: ID!) {
product(id: $id) {
id
title
options {
id
name
values
position
}
variants(first: 100) {
nodes {
id
title
sku
price
selectedOptions { name value }
metafields(first: 20, namespace: "custom") {
nodes { key value type }
}
}
pageInfo { hasNextPage endCursor }
}
}
}
productSet — the modern all-in-one mutation
Since 2025-01 API, productSet is the recommended atomic mutation for creating or fully replacing a product's options + variants + media in one operation:
mutation ProductSet($input: ProductSetInput!) {
productSet(input: $input) {
product {
id
title
variants(first: 250) {
nodes { id sku selectedOptions { name value } }
}
}
userErrors { field message }
}
}
Variables:
{
"input": {
"title": "Personalized Mug",
"productOptions": [
{ "name": "Color", "values": [{"name": "White"}, {"name": "Black"}] },
{ "name": "Size", "values": [{"name": "11oz"}, {"name": "15oz"}] }
],
"variants": [
{"optionValues": [{"optionName":"Color","name":"White"},{"optionName":"Size","name":"11oz"}], "price": "14.99", "sku": "MUG-W-11"},
{"optionValues": [{"optionName":"Color","name":"White"},{"optionName":"Size","name":"15oz"}], "price": "17.99", "sku": "MUG-W-15"},
{"optionValues": [{"optionName":"Color","name":"Black"},{"optionName":"Size","name":"11oz"}], "price": "14.99", "sku": "MUG-B-11"},
{"optionValues": [{"optionName":"Color","name":"Black"},{"optionName":"Size","name":"15oz"}], "price": "17.99", "sku": "MUG-B-15"}
]
}
}
The 2,048 variant cap
The variant cap raised from 100 to 2,048 on October 15, 2025. If you're operating on products with 500+ variants, you MUST paginate — the variants(first: N) connection now requires first: 250 maximum per page and you iterate via pageInfo.endCursor.
variants(first: 250) returned all variants must now handle pagination. Every read of a 500+ variant product breaks silently — the code gets the first 250, thinks it has all, and misses the rest.Metafields for personalization data
Custom personalization data (per-variant DPI requirements, POD partner routing, brand-approved logos) lives in metafields. Use metafield definitions to enforce schema:
mutation CreateMetafieldDefinition {
metafieldDefinitionCreate(definition: {
namespace: "personalization"
key: "engraving_max_chars"
name: "Engraving max chars"
type: "number_integer"
ownerType: PRODUCTVARIANT
validations: [{ name: "min", value: "1" }, { name: "max", value: "50" }]
}) {
createdDefinition { id }
userErrors { field message }
}
}
mutation SetVariantMetafield {
metafieldsSet(metafields: [{
ownerId: "gid://shopify/ProductVariant/40123",
namespace: "personalization",
key: "engraving_max_chars",
value: "20",
type: "number_integer"
}]) {
metafields { id key value }
userErrors { field message }
}
}
Bulk operations for large catalogs
Updating 5,000 variants one at a time hits rate limits fast. Use bulkOperationRunMutation for CSV-scale operations:
mutation StartBulkImport {
bulkOperationRunMutation(
mutation: "mutation ($input: ProductVariantsBulkInput!) { productVariantsBulkUpdate(...) { ... } }",
stagedUploadPath: "..."
) { bulkOperation { id status } }
}
query CheckBulkStatus {
currentBulkOperation { id status errorCode createdAt completedAt url }
}
Rate limits
| API | Rate limit | Notes |
|---|---|---|
| Admin GraphQL | 1,000-2,000 cost points/min (bucket) | Complex queries cost more points; use @include to skip fields you don't need |
| Admin REST | 40 requests/sec (leaky bucket) | Deprecated for new products/variants |
| Bulk Operations | 1 concurrent bulk operation per app per shop | Long-running, ideal for 1000+ item mutations |
Webhooks for option changes
Subscribe to products/update and product_variants/update webhooks to react to merchant option changes in real time — regenerating printful mockups, updating fulfillment metafields, or syncing to external ERP. See webhook variant changes for the full pattern.
Common errors
- "Options cannot be edited on a product with variants": use
productSetwhich handles the full state atomically, or delete variants first viaproductVariantsBulkDelete. - "Exceeds variant limit": 2,048 hard cap. If you legitimately need more, split into multiple products.
- "Metafield type mismatch": the value type must match the metafield definition exactly.
"20"as string for anumber_integerdefinition fails.
Frequently asked questions
Does Shopify Admin API support product option management?
Yes — via GraphQL. Use productSet to atomically create/update options + variants, metafieldsSet for per-variant personalization metadata, and bulkOperationRunMutation for large-scale updates.
Is REST or GraphQL better for options in 2026?
GraphQL — REST product endpoints were deprecated for new apps on February 1, 2025. All new option-management code should use GraphQL exclusively.
What's the Shopify variant limit and how does it affect API code?
2,048 variants per product as of October 15, 2025 (up from 100). This means your API code MUST paginate variant queries — an unpaginated query on a 500+ variant product silently returns only 250.
How do I store custom personalization data on variants?
Use metafield definitions. Create a definition for each personalization data point (engraving_max_chars, dpi_requirement, pod_partner) with typed validation, then set values per variant via metafieldsSet.
What API scopes does a personalization app need?
read_products, write_products, read_orders, write_orders, read_inventory, write_inventory, read_metaobjects, write_metaobjects. Add write_cart_transforms for Cart Transform functions and write_files for photo-upload personalization.
How do I handle Admin API rate limits at scale?
GraphQL uses a cost-based bucket (1,000-2,000 cost points/min). Complex queries cost more. Use bulk operations for 1000+ item mutations. Add exponential backoff on 429 responses.
What's the difference between productSet and productUpdate?
productSet is atomic and full-state — you pass the entire product spec (options + variants + media) and Shopify replaces it. productUpdate is partial. For personalization apps managing options + variants together, productSet is safer.
How do I test Admin API changes without breaking a live store?
Use a Shopify development store (free via Partners dashboard). Test all mutations against it, then promote to production. Never test destructive mutations (productDelete, productVariantsBulkDelete) on live data.
Related reading
- Shopify GraphQL for variants — variant-focused reference
- Storefront API for variants — client-side variant queries
- Metafield API for options — metafield deep dive
- Webhook variant changes — react to option updates
Try it free on Shopify
Print It My Way's permanent Free plan uses Admin GraphQL API + metafields + Cart Transform — the modern option-management stack, no code required.
Install Print It My Way