Shopify admin API for WooCommerce developers (2026)
How WooCommerce developers use the Shopify Admin API — GraphQL vs REST, product mutations, inventory management, order queries, webhooks, and porting WooCommerce REST API logic to Shopify.
WooCommerce developers building custom integrations used the WooCommerce REST API v3 — a REST interface for products, orders, customers, and coupons. Shopify offers two parallel Admin APIs: GraphQL (recommended) and REST (legacy). This guide bridges WooCommerce REST API patterns to their Shopify Admin API equivalents, covering the mutations and queries you'll use most when building migrations, integrations, and backend tools.
WooCommerce REST API vs Shopify Admin API
| Operation | WooCommerce REST v3 | Shopify Admin API (GraphQL) |
|---|---|---|
| List products | GET /wp-json/wc/v3/products | query { products(first: 50) } |
| Create product | POST /wp-json/wc/v3/products | mutation productCreate |
| Update product | PUT /wp-json/wc/v3/products/{id} | mutation productUpdate |
| Delete product | DELETE /wp-json/wc/v3/products/{id} | mutation productDelete |
| List orders | GET /wp-json/wc/v3/orders | query { orders(first: 50) } |
| Create order | POST /wp-json/wc/v3/orders | mutation orderCreate |
| Update inventory | PUT /products/{id} stock_quantity | mutation inventorySetQuantities |
| List customers | GET /wp-json/wc/v3/customers | query { customers(first: 50) } |
| Set metafields | POST /wp-json/wc/v3/products/{id} meta_data | mutation metafieldsSet |
| Webhooks | WooCommerce webhook manager | mutation webhookSubscriptionCreate |
Authentication
The Admin API requires a Private App access token (not the Storefront Access Token):
- Create via: Admin → Apps → Develop apps → create custom app → Admin API permissions
- Grant only required scopes:
write_products,read_orders, etc. - Access token:
shpat_xxxx(Admin API access token — keep server-side only, never expose client-side)
const SHOP_DOMAIN = 'yourstore.myshopify.com';
const ACCESS_TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN;
const endpoint = 'https://' + SHOP_DOMAIN + '/admin/api/2024-01/graphql.json';
async function adminQuery(query, variables = {}) {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': ACCESS_TOKEN,
},
body: JSON.stringify({ query, variables }),
});
const data = await response.json();
if (data.errors) throw new Error(JSON.stringify(data.errors));
return data;
}
Product mutations
Create a product (productCreate)
const CREATE_PRODUCT = `
mutation productCreate($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
variants(first: 10) {
edges { node { id sku price } }
}
}
userErrors { field message }
}
}
`;
const result = await adminQuery(CREATE_PRODUCT, {
input: {
title: "Classic leather wallet",
bodyHtml: "<p>Full grain leather, 6 card slots</p>",
vendor: "Artisan Co",
productType: "Accessories",
tags: ["leather", "wallet", "accessories"],
status: "ACTIVE",
variants: [{
price: "49.99",
sku: "WALLET-BLK-001",
inventoryManagement: "SHOPIFY",
inventoryQuantities: [{
availableQuantity: 50,
locationId: "gid://shopify/Location/12345"
}]
}]
}
});
Update a product
const UPDATE_PRODUCT = `
mutation productUpdate($input: ProductInput!) {
productUpdate(input: $input) {
product { id title }
userErrors { field message }
}
}
`;
await adminQuery(UPDATE_PRODUCT, {
input: {
id: "gid://shopify/Product/12345",
title: "Updated title",
tags: ["leather", "wallet", "new-arrival"]
}
});
Variable products: create with options and variants
const CREATE_VARIABLE_PRODUCT = `
mutation productCreate($input: ProductInput!) {
productCreate(input: $input) {
product { id title variants(first: 50) { edges { node { id title sku } } } }
userErrors { field message }
}
}
`;
await adminQuery(CREATE_VARIABLE_PRODUCT, {
input: {
title: "Classic t-shirt",
options: ["Color", "Size"],
variants: [
{ options: ["Black", "S"], price: "29.99", sku: "TSHIRT-BLK-S", inventoryQuantities: [{ availableQuantity: 10, locationId: "gid://shopify/Location/12345" }] },
{ options: ["Black", "M"], price: "29.99", sku: "TSHIRT-BLK-M", inventoryQuantities: [{ availableQuantity: 15, locationId: "gid://shopify/Location/12345" }] },
{ options: ["White", "S"], price: "29.99", sku: "TSHIRT-WHT-S", inventoryQuantities: [{ availableQuantity: 8, locationId: "gid://shopify/Location/12345" }] },
{ options: ["White", "M"], price: "29.99", sku: "TSHIRT-WHT-M", inventoryQuantities: [{ availableQuantity: 12, locationId: "gid://shopify/Location/12345" }] }
]
}
});
Metafields (replaces WooCommerce meta_data)
WooCommerce stored custom data as meta_data arrays on products. Shopify uses structured metafields with namespace + key + type:
const SET_METAFIELDS = `
mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields { namespace key value type }
userErrors { field message }
}
}
`;
await adminQuery(SET_METAFIELDS, {
metafields: [
{
ownerId: "gid://shopify/Product/12345",
namespace: "product_info",
key: "material",
value: "Full grain leather",
type: "single_line_text_field"
},
{
ownerId: "gid://shopify/Product/12345",
namespace: "product_info",
key: "weight_g",
value: "85",
type: "number_integer"
}
]
});
Inventory management
WooCommerce stores stock_quantity on the product. Shopify separates inventory into InventoryItem (the item) and InventoryLevel (quantity per location):
// Get location ID first
const LOCATIONS_QUERY = `
query { locations(first: 10) { edges { node { id name } } } }
`;
// Set inventory quantity
const SET_INVENTORY = `
mutation inventorySetQuantities($input: InventorySetQuantitiesInput!) {
inventorySetQuantities(input: $input) {
inventoryAdjustmentGroup { id }
userErrors { field message }
}
}
`;
await adminQuery(SET_INVENTORY, {
input: {
name: "available",
reason: "correction",
quantities: [{
inventoryItemId: "gid://shopify/InventoryItem/67890",
locationId: "gid://shopify/Location/12345",
quantity: 25
}]
}
});
Order queries
const ORDERS_QUERY = `
query getOrders($first: Int!, $after: String) {
orders(first: $first, after: $after, sortKey: CREATED_AT, reverse: true) {
pageInfo { hasNextPage endCursor }
edges {
node {
id
name
createdAt
totalPriceSet { shopMoney { amount currencyCode } }
displayFinancialStatus
displayFulfillmentStatus
customer { id email firstName lastName }
lineItems(first: 20) {
edges {
node {
id title quantity
variant { sku price }
}
}
}
}
}
}
}
`;
let allOrders = [];
let cursor = null;
let hasMore = true;
while (hasMore) {
const result = await adminQuery(ORDERS_QUERY, { first: 50, after: cursor });
const { orders } = result.data;
allOrders = allOrders.concat(orders.edges.map(e => e.node));
hasMore = orders.pageInfo.hasNextPage;
cursor = orders.pageInfo.endCursor;
}
Webhooks (replaces WooCommerce hooks)
WooCommerce used action hooks (woocommerce_order_status_changed, woocommerce_new_order). Shopify has HTTP webhooks:
const CREATE_WEBHOOK = `
mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
webhookSubscription { id callbackUrl topic }
userErrors { field message }
}
}
`;
// Subscribe to new orders
await adminQuery(CREATE_WEBHOOK, {
topic: "ORDERS_CREATE",
webhookSubscription: {
callbackUrl: "https://yourapp.com/webhooks/shopify/orders",
format: "JSON"
}
});
// Available topics: PRODUCTS_CREATE, PRODUCTS_UPDATE, ORDERS_PAID,
// ORDERS_FULFILLED, CUSTOMERS_CREATE, INVENTORY_LEVELS_UPDATE, etc.
Rate limits
| API | Rate limit type | Limit | Restore rate |
|---|---|---|---|
| GraphQL Admin API | Cost-based throttling | 1000 points/bucket | 50 points/second |
| REST Admin API | Leaky bucket | 40 requests | 2 requests/second |
| Storefront API | Cost-based throttling | 1000 points/bucket | 50 points/second |
GraphQL response includes cost information in extensions:
// Check cost in response
const result = await adminQuery(PRODUCTS_QUERY, { first: 50 });
const { actualQueryCost, throttleStatus } = result.extensions.cost;
// throttleStatus: { maximumAvailable, currentlyAvailable, restoreRate }
// If currentlyAvailable is low, add delay: await new Promise(r => setTimeout(r, 1000))
Bulk operations (large catalog migrations)
For migrating catalogs with 1000+ products, use Shopify Bulk Operations — asynchronous operations that return a JSONL file:
// 1. Start bulk operation query
const BULK_QUERY = `
mutation {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id title handle
variants(first: 100) { edges { node { id sku price inventoryQuantity } } }
}
}
}
}
"""
) {
bulkOperation { id status }
userErrors { field message }
}
}
`;
// 2. Poll for completion
const POLL_BULK = `
query {
currentBulkOperation {
id status errorCode
createdAt completedAt
objectCount url
}
}
`;
// 3. When status === "COMPLETED", download the JSONL from url
// Each line is one product node as JSON
Developer migration checklist
- Create Admin API access token with only required scopes
- Never expose Admin API token client-side — server-side only (unlike Storefront token)
- Port WooCommerce REST API product CRUD to Shopify productCreate / productUpdate mutations
- Port custom meta_data fields to metafieldsSet with correct type (single_line_text_field, number_integer, boolean, etc.)
- Fetch default location ID for inventorySetQuantities
- Implement cost-based rate limit handling for GraphQL (check throttleStatus in response)
- Replace WooCommerce action hooks with Shopify webhook subscriptions (webhookSubscriptionCreate)
- For large catalogs: use Bulk Operations API instead of paginated queries
- Handle userErrors in every mutation response — Shopify returns 200 even for failed mutations
- Test all mutations on a Shopify Development Store before production
The most common mistake WooCommerce developers make on the Shopify Admin API is ignoring userErrors in mutation responses. Unlike WooCommerce REST API which returns 4xx HTTP status codes for validation failures, Shopify GraphQL mutations always return HTTP 200 — validation errors are in the userErrors array on the mutation result. A product that failed to create still returns 200 with an empty product and populated userErrors. Always check userErrors on every mutation or you'll silently lose products during migration.
Migrate your store with k-sync
Connect your WooCommerce store, validate your products, and push to Shopify in minutes. Free for up to 50 products.
Get started freeRelated reading
Migrating a luggage and travel accessories store from WooCommerce to Shopify (2026)
How to migrate a luggage, travel bags, or travel accessories WooCommerce store to Shopify — luggage specifications, airline compliance, TSA lock, warranty and durability claims, and luggage retail Shopify setup.
Migrating a motorcycle accessories store from WooCommerce to Shopify (2026)
How to migrate a motorcycle accessories, biker gear, or motorbike parts WooCommerce store to Shopify — helmet safety standards, CE-rated protective clothing, type approval for parts, fitment compatibility, and motorcycle retail Shopify setup.