
Shopify Admin Api
- 67 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Automate Shopify store operations for products, orders, inventory, and customers using the GraphQL Admin API with bulk operations.
About
Automates Shopify products, orders, inventory, and customers via the GraphQL Admin API including bulk operations. A developer uses it to script or integrate backend store operations.
- GraphQL Admin API for products, orders, inventory, customers
- Bulk operation support for large datasets
Shopify Admin Api by the numbers
- 67 all-time installs (skills.sh)
- Ranked #3,098 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill shopify-admin-apiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 67 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Automate Shopify store operations for products, orders, inventory, and customers using the GraphQL Admin API with bulk operations.
Files
Shopify Admin API
Overview
The Shopify Admin API gives apps full access to a merchant's store data — products, variants, orders, customers, inventory, metafields, and more. It is available in both GraphQL (recommended) and REST flavors, with GraphQL offering precise field selection, bulk operations, and better rate limiting via the calculated cost system. Use the @shopify/shopify-api Node.js library or direct HTTP calls with an Admin API access token.
When to Use This Skill
- When reading or writing product catalog data (titles, variants, pricing, images, inventory)
- When fulfilling or updating orders programmatically from an external system
- When syncing customer records between Shopify and a CRM or ERP
- When running bulk data exports or imports using Bulk Operations
- When building an internal tool that needs merchant store access via a Custom App token
- When automating inventory adjustments from a warehouse management system
Core Instructions
1. Obtain an Admin API access token
For a custom app (single store), create it in Admin → Settings → Apps and Sales Channels → Develop apps. For a public/partner app, the token is obtained after OAuth (see @shopify-app-development).
# .env
SHOPIFY_ADMIN_API_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SHOPIFY_SHOP=mystore.myshopify.com
SHOPIFY_API_VERSION=2025-012. Initialize the Admin API client
// lib/shopify-admin.ts
import { shopifyApi, ApiVersion, Session } from "@shopify/shopify-api";
import "@shopify/shopify-api/adapters/node";
const shopify = shopifyApi({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
scopes: ["read_products", "write_products", "read_orders", "write_orders"],
hostName: process.env.SHOPIFY_APP_URL!,
apiVersion: ApiVersion.January25,
isEmbeddedApp: false, // true for merchant-facing embedded apps
});
// For custom apps with a static token
const session = new Session({
id: `offline_${process.env.SHOPIFY_SHOP}`,
shop: process.env.SHOPIFY_SHOP!,
state: "",
isOnline: false,
accessToken: process.env.SHOPIFY_ADMIN_API_TOKEN,
});
export const adminClient = new shopify.clients.Graphql({ session });
export const restClient = new shopify.clients.Rest({ session });3. Query products with GraphQL
// Fetch products with variants and inventory
export async function getProducts(cursor?: string) {
const response = await adminClient.request(`
query GetProducts($cursor: String) {
products(first: 50, after: $cursor) {
pageInfo { hasNextPage endCursor }
edges {
node {
id
title
status
variants(first: 100) {
edges {
node {
id
sku
price
inventoryQuantity
inventoryItem { id }
}
}
}
}
}
}
}
`, { variables: { cursor } });
return response.data.products;
}
// Update a product's price via mutation
export async function updateVariantPrice(variantId: string, price: string) {
const response = await adminClient.request(`
mutation UpdateVariantPrice($id: ID!, $price: Money!) {
productVariantUpdate(input: { id: $id, price: $price }) {
productVariant { id price }
userErrors { field message }
}
}
`, { variables: { id: variantId, price } });
const { userErrors } = response.data.productVariantUpdate;
if (userErrors.length > 0) throw new Error(userErrors[0].message);
return response.data.productVariantUpdate.productVariant;
}4. Fetch and update orders
// Query unfulfilled orders
export async function getUnfulfilledOrders() {
const response = await adminClient.request(`
query {
orders(first: 50, query: "fulfillment_status:unfulfilled financial_status:paid") {
edges {
node {
id
name
email
createdAt
lineItems(first: 50) {
edges {
node {
title
quantity
variant { id sku }
}
}
}
shippingAddress {
firstName lastName address1 city province zip country
}
}
}
}
}
`);
return response.data.orders.edges.map(({ node }: any) => node);
}
// Mark an order as fulfilled
export async function fulfillOrder(orderId: string, trackingNumber: string, trackingCompany: string) {
// First get fulfillment order ID
const orderResponse = await adminClient.request(`
query GetFulfillmentOrders($id: ID!) {
order(id: $id) {
fulfillmentOrders(first: 5) {
edges {
node { id status lineItems(first: 20) { edges { node { id remainingQuantity } } } }
}
}
}
}
`, { variables: { id: orderId } });
const fulfillmentOrder = orderResponse.data.order.fulfillmentOrders.edges[0]?.node;
if (!fulfillmentOrder) throw new Error("No fulfillment order found");
const response = await adminClient.request(`
mutation FulfillOrder($fulfillment: FulfillmentInput!) {
fulfillmentCreate(fulfillment: $fulfillment) {
fulfillment { id status }
userErrors { field message }
}
}
`, {
variables: {
fulfillment: {
lineItemsByFulfillmentOrder: [{ fulfillmentOrderId: fulfillmentOrder.id }],
trackingInfo: { number: trackingNumber, company: trackingCompany },
notifyCustomer: true,
},
},
});
return response.data.fulfillmentCreate;
}5. Run Bulk Operations for large datasets
For exporting thousands of products or orders, use Bulk Operations (GraphQL only) — they run asynchronously and return a JSONL file URL:
// Start a bulk operation
export async function startBulkProductExport() {
const response = await adminClient.request(`
mutation {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id title status
variants {
edges {
node { id sku price inventoryQuantity }
}
}
}
}
}
}
"""
) {
bulkOperation { id status }
userErrors { field message }
}
}
`);
return response.data.bulkOperationRunQuery.bulkOperation;
}
// Poll for completion and download URL
export async function getBulkOperationStatus() {
const response = await adminClient.request(`
query {
currentBulkOperation {
id status errorCode
objectCount
url # JSONL download URL — available when status is COMPLETED
}
}
`);
return response.data.currentBulkOperation;
}Examples
Customer search and update via REST API
// REST is still valid for simple lookups where GraphQL overhead isn't worth it
export async function searchCustomers(email: string) {
const response = await restClient.get({
path: "customers/search",
query: { query: `email:${email}` },
});
return response.body.customers;
}
export async function tagCustomer(customerId: number, tags: string[]) {
const response = await restClient.put({
path: `customers/${customerId}`,
data: { customer: { id: customerId, tags: tags.join(",") } },
});
return response.body.customer;
}Inventory adjustment
export async function adjustInventory(inventoryItemId: string, locationId: string, delta: number) {
const response = await adminClient.request(`
mutation AdjustInventory($input: InventoryAdjustQuantitiesInput!) {
inventoryAdjustQuantities(input: $input) {
inventoryAdjustmentGroup {
changes {
name
delta
item { id }
location { name }
}
}
userErrors { field message }
}
}
`, {
variables: {
input: {
name: "available",
reason: "correction",
changes: [
{
inventoryItemId,
locationId,
delta,
},
],
},
},
});
return response.data.inventoryAdjustQuantities;
}Best Practices
- Prefer GraphQL over REST — GraphQL has a cost-based rate limit (1000 cost units/second) that's more forgiving than REST's 40 requests/second; it also avoids over-fetching
- Use Bulk Operations for exports above 250 records — never page through thousands of records manually; Bulk Operations handle up to millions of records in a single async job
- Always handle `userErrors` on mutations — a 200 HTTP response does not mean success; check
userErrorsarray before treating a mutation as successful - Use `gid://shopify/Product/123` format for IDs — Admin API GraphQL uses global IDs; never send numeric IDs without the GID prefix
- Implement exponential backoff — respect
Retry-Afterheaders and implement backoff on 429 and 503 responses - Cache immutable product data — product titles and handles rarely change; cache them with a reasonable TTL to reduce API calls
- Scope to minimum required permissions — requesting fewer scopes reduces merchant trust friction at install time
Common Pitfalls
| Problem | Solution |
|---|---|
Cost exceeds bucket size GraphQL error | Reduce the first: argument on connections (use 50 instead of 250) or restructure the query to avoid deeply nested connections |
| Numeric vs GID ID format mismatch | Always convert REST numeric IDs to GID format: gid://shopify/Product/${numericId} |
| Bulk operation URL returns 403 | The JSONL URL is a time-limited signed S3 URL — download it immediately after polling COMPLETED status |
Order fulfillment fails with FULFILLMENT_ORDER_NOT_FOUND | Orders must be fulfilled via Fulfillment Orders API (not legacy Fulfillments API) since API version 2022-07 |
| Webhook events trigger duplicate processing | Use the admin_graphql_api_id in webhook payloads and implement idempotency keying |
| Customer update clears existing tags | When updating tags, always fetch current tags first and append — the API replaces, not merges |
Related Skills
- @shopify-app-development
- @shopify-webhooks
- @shopify-metafields
- @shopify-storefront-api
- @bulk-data-operations
{
"context": "Tests whether the agent uses the Shopify Bulk Operations API for large-scale product exports rather than paginating manually, correctly polls for completion, and uses the proper library and GID-format IDs.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Uses @shopify/shopify-api",
"max_score": 8,
"description": "Script imports and uses the `@shopify/shopify-api` package (not raw fetch/axios/got alone), visible in imports or package.json dependencies"
},
{
"name": "Imports node adapter",
"max_score": 8,
"description": "Code includes `import '@shopify/shopify-api/adapters/node'` or `require('@shopify/shopify-api/adapters/node')`"
},
{
"name": "Uses bulkOperationRunQuery",
"max_score": 12,
"description": "Script triggers a bulk export using the `bulkOperationRunQuery` GraphQL mutation rather than paginated queries"
},
{
"name": "Polls currentBulkOperation",
"max_score": 10,
"description": "Script polls for completion using the `currentBulkOperation` GraphQL query (checking `status` field)"
},
{
"name": "Downloads URL on COMPLETED",
"max_score": 10,
"description": "Script only fetches the JSONL download URL after the bulk operation status is COMPLETED (not before or with a long delay)"
},
{
"name": "Writes JSONL output",
"max_score": 8,
"description": "The script writes or is clearly intended to write results to a `.jsonl` file (one JSON object per line)"
},
{
"name": "Uses January25 API version",
"max_score": 8,
"description": "Code sets the API version to `ApiVersion.January25` or the string `'2025-01'`"
},
{
"name": "Offline session for custom app",
"max_score": 8,
"description": "Session is created with `isOnline: false` for a custom app with static token"
},
{
"name": "GraphQL client for bulk op",
"max_score": 8,
"description": "The bulk operation mutation is issued via a GraphQL client (not a REST client)"
},
{
"name": "No manual pagination",
"max_score": 10,
"description": "Code does NOT use cursor-based pagination (no looping over `pageInfo.hasNextPage` / `endCursor`) as the primary export mechanism for the full catalog"
},
{
"name": "Product fields requested",
"max_score": 10,
"description": "Bulk operation query requests at least: product id, title, status and variant fields (id, sku, price, inventoryQuantity)"
}
]
}
Large-Scale Product Catalog Export
Problem/Feature Description
A mid-sized e-commerce company runs a Shopify store with over 8,000 active products. Their data analytics team needs a complete snapshot of the product catalog — including each product's ID, title, status, and all variant details (SKU, price, inventory quantity) — to feed into their business intelligence dashboard. Previously, a developer tried to fetch everything with paginated API calls and the job kept timing out or hitting rate limits after a few hundred products.
The team wants a Node.js TypeScript script that reliably exports all products to a local JSONL file. The script should be executable from the command line, handle the entire product catalog without manual pagination, and be robust enough to run as a nightly job. Since the exported file will eventually be piped downstream, it should write results only when the export is fully finished. The team also wants to understand how the process works, so the script should log its progress to the console.
Output Specification
Produce a working TypeScript script (export-products.ts) that exports all products from a Shopify store. The script should:
- Accept Shopify store credentials via environment variables (
SHOPIFY_ADMIN_API_TOKEN,SHOPIFY_SHOP,SHOPIFY_API_KEY,SHOPIFY_API_SECRET,SHOPIFY_APP_URL) - Export product data including id, title, status, and all variants (id, sku, price, inventoryQuantity)
- Write the results to
products-export.jsonl(one JSON object per line) - Print progress/status messages to stdout as it runs
Also produce a brief README.md explaining how to install dependencies and run the script.
{
"context": "Tests whether the agent fetches existing customer tags before updating (to avoid clearing them), uses REST for simple customer lookup, handles rate limiting with exponential backoff, and uses the correct Shopify library.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Uses @shopify/shopify-api",
"max_score": 7,
"description": "Code imports and uses the `@shopify/shopify-api` package (not raw fetch/axios alone), visible in imports or package.json"
},
{
"name": "Imports node adapter",
"max_score": 7,
"description": "Code includes `import '@shopify/shopify-api/adapters/node'` or `require('@shopify/shopify-api/adapters/node')`"
},
{
"name": "Fetches current tags first",
"max_score": 14,
"description": "Before updating a customer's tags, code fetches the customer's existing tags (via GET request or GraphQL query) rather than writing tags directly"
},
{
"name": "Appends tag, does not replace",
"max_score": 14,
"description": "Code merges/appends the new tag to the existing tags list — does NOT send only the new tag as the complete tags value"
},
{
"name": "REST for customer lookup",
"max_score": 8,
"description": "Customer lookup by email uses the REST API (`customers/search` endpoint) rather than a GraphQL query"
},
{
"name": "Exponential backoff on 429",
"max_score": 12,
"description": "Code implements retry logic with exponential backoff (or a delay) when a 429 (Too Many Requests) response is received"
},
{
"name": "Respects Retry-After header",
"max_score": 8,
"description": "Rate-limiting retry logic reads or references the `Retry-After` header to determine the wait duration"
},
{
"name": "Offline session",
"max_score": 7,
"description": "Session is created with `isOnline: false` for a custom app with static token"
},
{
"name": "Uses January25 API version",
"max_score": 7,
"description": "Code sets the API version to `ApiVersion.January25` or the string `'2025-01'`"
},
{
"name": "Writes tagging-results.json",
"max_score": 8,
"description": "Script writes a results file (`tagging-results.json` or similar) with per-customer outcome information"
},
{
"name": "Tags joined as comma string",
"max_score": 8,
"description": "When updating customer tags via REST PUT, tags are joined as a comma-separated string (not an array), matching the Shopify REST API format"
}
]
}
Customer Loyalty Tagging System
Problem/Feature Description
An e-commerce brand wants to implement a loyalty tier system in their Shopify store. After analyzing purchase history in their CRM, they have identified customers who qualify for "vip", "gold", or "silver" loyalty tiers. They need a script that applies these loyalty tags to the corresponding customer records in Shopify. Customer profiles may already carry labels from previous campaigns (such as "newsletter", "wholesale", "eu-customer"), and these must not be lost in the process.
The operation needs to handle a list of customer records, look each customer up by email address, and apply the appropriate loyalty tag. Since this involves many sequential API calls, the script must be resilient to rate limiting. The merchant has seen API errors before and needs the script to recover gracefully rather than failing partway through the list.
Output Specification
Produce a TypeScript script (tag-customers.ts) that:
- Reads a list of customer tag assignments from a JSON file (
customers.json) - For each customer, finds the customer in Shopify by email and adds the specified tag without disrupting any labels they already have
- Writes a results summary to
tagging-results.jsonwith the outcome for each customer (success/failure and final tags) - Handles rate limiting gracefully
Also produce a package.json with necessary dependencies.
Input Files
The following files are provided as inputs. Extract them before beginning.
=============== FILE: customers.json =============== [ { "email": "alice@example.com", "tag": "vip" }, { "email": "bob@example.com", "tag": "gold" }, { "email": "carol@example.com", "tag": "silver" }, { "email": "david@example.com", "tag": "vip" }, { "email": "eve@example.com", "tag": "gold" } ]
{
"context": "Tests whether the agent uses the Fulfillment Orders API (not the legacy Fulfillments API) to fulfill orders, correctly handles userErrors on mutations, uses GID format for IDs, and initializes the Shopify client properly for a custom app.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Uses @shopify/shopify-api",
"max_score": 7,
"description": "Code imports and uses the `@shopify/shopify-api` package (not raw HTTP fetch/axios alone), visible in imports or package.json"
},
{
"name": "Imports node adapter",
"max_score": 7,
"description": "Code includes `import '@shopify/shopify-api/adapters/node'` or `require('@shopify/shopify-api/adapters/node')`"
},
{
"name": "Fetches fulfillmentOrders first",
"max_score": 12,
"description": "Code queries `fulfillmentOrders` on the order before attempting to create a fulfillment (two-step process: look up fulfillment order ID, then fulfill)"
},
{
"name": "Uses fulfillmentCreate mutation",
"max_score": 12,
"description": "Fulfillment is created using the `fulfillmentCreate` GraphQL mutation (NOT `fulfillmentOrderFulfill` or legacy `POST /fulfillments` REST endpoint)"
},
{
"name": "Does NOT use legacy REST fulfillment",
"max_score": 8,
"description": "Code does NOT call the REST `/orders/{id}/fulfillments` endpoint or `restClient.post` for creating fulfillments"
},
{
"name": "Checks userErrors on mutation",
"max_score": 12,
"description": "Code checks the `userErrors` array from the `fulfillmentCreate` response and throws or returns an error if `userErrors.length > 0`"
},
{
"name": "GID format for order ID",
"max_score": 10,
"description": "When a numeric order ID is received, code converts it to GID format (`gid://shopify/Order/${id}`) before using it in GraphQL queries/mutations"
},
{
"name": "Offline session",
"max_score": 7,
"description": "Session is created with `isOnline: false` for the custom app with static token"
},
{
"name": "Uses January25 API version",
"max_score": 7,
"description": "Code sets the API version to `ApiVersion.January25` or the string `'2025-01'`"
},
{
"name": "notifyCustomer included",
"max_score": 8,
"description": "The `fulfillmentCreate` call includes `notifyCustomer: true` in the fulfillment input"
},
{
"name": "Tracking info passed",
"max_score": 10,
"description": "The fulfillment input includes `trackingInfo` with `number` and `company` fields"
}
]
}
Automated Order Fulfillment Service
Problem/Feature Description
A Shopify merchant runs a small warehouse operation. When orders are picked and packed, warehouse staff scan a barcode to generate a tracking number and want the system to automatically mark the corresponding Shopify order as fulfilled and notify the customer. Currently they do this manually through the Shopify admin dashboard, which is slow and error-prone.
You need to build a Node.js TypeScript module that can be called by the warehouse's internal system to fulfill a Shopify order given its numeric order ID and a tracking number. The module should look up any unfulfilled orders, and for a given order mark it as fulfilled with the provided tracking number. The merchant has a custom Shopify app with a static Admin API access token — there is no OAuth flow involved. The implementation needs to be production-quality: it should handle cases where the operation fails gracefully, including surface errors back to the caller clearly.
Output Specification
Produce a TypeScript module (fulfill-order.ts) that exports:
- A function
fulfillOrder(orderId: string, trackingNumber: string, trackingCompany: string): Promise<void>that marks a Shopify order as fulfilled
Also produce a demo.ts script that demonstrates calling fulfillOrder with a sample order ID (e.g. "5678901234") and tracking details, printing success or error to the console. The demo should use environment variables for credentials (SHOPIFY_ADMIN_API_TOKEN, SHOPIFY_SHOP).
Include a package.json with the necessary dependencies.
{
"name": "finsi/shopify-admin-api",
"version": "0.1.0",
"summary": "Admin API for products, orders, customers with GraphQL and REST",
"skills": {
"shopify-admin-api": {
"path": "SKILL.md"
}
}
}