Automating Bulk Product Updates via the Admin REST API

Table of Contents
Big thanks to our contributors those make our blogs possible.

Our growing community of contributors bring their unique insights from around the world to power our blog. 

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

  1. 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).
  2. Developer Environment:
    • Python 3.7+ with requests installed (pip install requests)
    • Node.js 14+ with axios installed (npm install axios)
  3. 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

  1. Fetch Product IDs
  2. Divide into Batches (e.g., 50 products per batch)
  3. Prepare Update Payloads (e.g., new price, tag additions)
  4. Execute Batch Updates with Rate-Limit Handling
  5. Retry Failures
  6. 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.

Let's connect on TikTok

Join our newsletter to stay updated

Sydney Based Software Solutions Professional who is crafting exceptional systems and applications to solve a diverse range of problems for the past 10 years.

Share the Post

Related Posts