Introduction
When you manage a large catalog, manually editing each product in the Shopify Admin quickly becomes tedious and error-prone. Automating bulk updates via the Admin REST API streamlines tasks like price adjustments, inventory changes, or metadata tagging across hundreds or thousands of items in minutes. In this guide, you’ll learn how to structure your workflow, handle pagination and rate limits, and implement retry logic—complete with ready-to-use code samples in Python and Node.js.

Prerequisites
- Shopify Admin API Credentials: A private or custom app with
write_products
scope and its API key and password (for Basic Auth) or an access token (for OAuth). - Developer Environment:
- Python 3.7+ with
requests
installed (pip install requests
) - Node.js 14+ with
axios
installed (npm install axios
)
- Python 3.7+ with
- Familiarity with REST: Understanding of HTTP methods (GET, PUT) and JSON payloads.
1. Understand the Products Endpoint
- GET /admin/api/2025-01/products.json
Fetch a page of products (up to 250 per call). - PUT /admin/api/2025-01/products/{product_id}.json
Update a single product’s fields.
Since there’s no native “bulk update” endpoint in REST, you loop over product IDs and send individual updates in controlled batches.
2. Workflow Overview
- Fetch Product IDs
- Divide into Batches (e.g., 50 products per batch)
- Prepare Update Payloads (e.g., new price, tag additions)
- Execute Batch Updates with Rate-Limit Handling
- Retry Failures
- Log Successes & Errors
3. Handling Pagination
Shopify paginates product lists via the link
header. Use the limit
and page_info
parameters:
httpCopyEditGET /admin/api/2025-01/products.json?limit=250&page_info={cursor}
Continue fetching until no rel="next"
link remains.
4. Rate Limiting and Throttling
Shopify’s REST Admin uses a leaky-bucket algorithm—40 points per app, refilled at 2 points/second.

- GET costs 1 point.
- PUT costs 2 points.
Implement a simple throttle by pausing when your remaining bucket drops below a threshold.
5. Example: Python Script
pythonCopyEditimport time, requests
SHOP = "your-shop.myshopify.com"
API_VERSION = "2025-01"
ACCESS_TOKEN = "shpat_xxx" # or use API key/password for Basic Auth
HEADERS = {
"X-Shopify-Access-Token": ACCESS_TOKEN,
"Content-Type": "application/json",
}
def get_products():
products = []
url = f"https://{SHOP}/admin/api/{API_VERSION}/products.json?limit=250"
while url:
resp = requests.get(url, headers=HEADERS)
resp.raise_for_status()
data = resp.json()
products.extend(data["products"])
link = resp.headers.get("Link", "")
next_link = None
for part in link.split(","):
if 'rel="next"' in part:
next_link = part[part.find("<")+1:part.find(">")]
url = next_link
time.sleep(0.5) # simple throttle
return products
def update_product(product_id, payload):
url = f"https://{SHOP}/admin/api/{API_VERSION}/products/{product_id}.json"
resp = requests.put(url, headers=HEADERS, json={"product": payload})
return resp
def bulk_update_price(percentage_increase):
all_products = get_products()
batch_size = 50
for i in range(0, len(all_products), batch_size):
batch = all_products[i:i+batch_size]
for prod in batch:
new_price = float(prod["variants"][0]["price"]) * (1 + percentage_increase/100)
payload = {
"id": prod["id"],
"variants": [{"id": prod["variants"][0]["id"], "price": f"{new_price:.2f}"}]
}
resp = update_product(prod["id"], payload)
if resp.status_code == 429:
# Rate limit hit: wait and retry
retry_after = int(resp.headers.get("Retry-After", 2))
time.sleep(retry_after)
resp = update_product(prod["id"], payload)
if resp.ok:
print(f"Updated {prod['id']}")
else:
print(f"Error {resp.status_code} for {prod['id']}: {resp.text}")
# Pause between batches
time.sleep(5)
if __name__ == "__main__":
bulk_update_price(10) # increase all prices by 10%
6. Example: Node.js Script
javascriptCopyEditconst axios = require("axios");
const SHOP = "your-shop.myshopify.com";
const API_VERSION = "2025-01";
const ACCESS_TOKEN = "shpat_xxx";
const client = axios.create({
baseURL: `https://${SHOP}/admin/api/${API_VERSION}`,
headers: {
"X-Shopify-Access-Token": ACCESS_TOKEN,
"Content-Type": "application/json",
},
});
async function getProducts() {
let products = [];
let url = "/products.json?limit=250";
while (url) {
const resp = await client.get(url);
products = products.concat(resp.data.products);
const link = resp.headers.link || "";
const next = link.split(",").find(s => s.includes('rel="next"'));
url = next ? next.match(/<([^>]+)>/)[1] : null;
await new Promise(r => setTimeout(r, 500));
}
return products;
}
async function updateProduct(productId, payload) {
try {
return await client.put(`/products/${productId}.json`, { product: payload });
} catch (err) {
if (err.response && err.response.status === 429) {
const retryAfter = parseInt(err.response.headers["retry-after"] || 2) * 1000;
await new Promise(r => setTimeout(r, retryAfter));
return client.put(`/products/${productId}.json`, { product: payload });
}
throw err;
}
}
async function bulkUpdatePrice(percentage) {
const products = await getProducts();
const batchSize = 50;
for (let i = 0; i < products.length; i += batchSize) {
const batch = products.slice(i, i + batchSize);
await Promise.all(batch.map(async prod => {
const current = parseFloat(prod.variants[0].price);
const updated = (current * (1 + percentage / 100)).toFixed(2);
const payload = {
id: prod.id,
variants: [{ id: prod.variants[0].id, price: updated }]
};
try {
await updateProduct(prod.id, payload);
console.log(`Updated ${prod.id}`);
} catch (e) {
console.error(`Failed ${prod.id}:`, e.response?.data || e.message);
}
}));
await new Promise(r => setTimeout(r, 5000));
}
}
bulkUpdatePrice(10);
7. Best Practices
- Test on a Small Subset: Always trial your script on a handful of products before a full run.
- Logging & Alerts: Record IDs of failed updates and notify via email/slack.
- Idempotency: Design your payloads so repeated runs don’t cause unintended side effects.
- Version Control: Lock your API version (e.g.,
2025-01
) to prevent breaking changes. - Backups: Export your current product data (CSV or JSON) before running mass updates.

Conclusion
Automating bulk product updates via the Admin REST API transforms a laborious chore into a repeatable, reliable process. By fetching products in pages, batching updates, respecting rate limits, and implementing retry logic, you’ll maintain catalog accuracy at scale—whether you’re applying price changes, updating inventory, or tagging products for a promotion. Start by adapting the sample scripts to your needs, and you’ll be running full-catalog updates in minutes instead of days.