Shopify: Give Every Product Photo a White Background Automatically (Admin API + Background Removal API)
Consistent white backgrounds lift conversion and are mandatory the moment you syndicate to Google Shopping, Amazon or Meta catalogues. Editing 800 SKUs by hand is not a plan. This is the automated version: a Node script that reads your product images through the Shopify Admin GraphQL API, sends each one to the bgclear API with a white fill, and attaches the result back to the product. It runs from your laptop or a cron; no Shopify app install required.
In this guide
What you need
A Shopify custom app (Settings → Apps and sales channels → Develop apps) with the read_products and write_products scopes and its Admin API access token; a bgclear API key from the dashboard (10 free full-resolution credits to start; preview size is free for a dry run); Node 18+. Shopify's productCreateMedia mutation accepts a public URL as originalSource, and the bgclear JSON response gives you exactly that — a hosted result URL valid for 24 hours — so no file juggling.
Step 1: list products and images
const SHOP = "your-store.myshopify.com";
const ADMIN = `https://${SHOP}/admin/api/2025-07/graphql.json`;
const shopify = async (query, variables = {}) => {
const res = await fetch(ADMIN, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Shopify-Access-Token": process.env.SHOPIFY_TOKEN },
body: JSON.stringify({ query, variables }),
});
const { data, errors } = await res.json();
if (errors) throw new Error(JSON.stringify(errors));
return data;
};
async function* products() {
let cursor = null;
do {
const data = await shopify(`query($cursor: String) {
products(first: 50, after: $cursor) {
pageInfo { hasNextPage endCursor }
nodes { id title media(first: 10) { nodes { id ... on MediaImage { image { url } } } } }
}
}`, { cursor });
for (const p of data.products.nodes) yield p;
cursor = data.products.pageInfo.hasNextPage ? data.products.pageInfo.endCursor : null;
} while (cursor);
}Step 2: remove the background with a white fill
JSON mode: pass the Shopify CDN URL as image_url, ask for bg_color=ffffff and format=jpg (Shopify re-encodes anyway, and JPG on white is the smallest), and use the product-image id as the idempotency key so a re-run never charges twice.
async function whiteBackground(imageUrl, idemKey) {
const res = await fetch("https://www.bgclear.ai/api/v1/remove", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BGCLEAR_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
"Idempotency-Key": idemKey,
},
body: JSON.stringify({ image_url: imageUrl, size: "auto", format: "jpg", bg_color: "ffffff" }),
});
if (res.status === 429) {
await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("X-RateLimit-Reset") ?? 5)));
return whiteBackground(imageUrl, idemKey);
}
if (!res.ok) throw new Error(`bgclear ${res.status}: ${(await res.json()).error.message}`);
return res.json(); // { url, width, height, credits_remaining, ... }
}size: "auto" processes images up to 4 megapixels at full resolution synchronously — most Shopify product images are 2048 px and fit. For larger originals, post to /api/v1/jobs instead (bulk guide).
Step 3: attach the result to the product
async function attach(productId, url, alt) {
const data = await shopify(`mutation($id: ID!, $media: [CreateMediaInput!]!) {
productCreateMedia(productId: $id, media: $media) {
media { id status }
mediaUserErrors { field message }
}
}`, { id: productId, media: [{ originalSource: url, mediaContentType: "IMAGE", alt }] });
const errs = data.productCreateMedia.mediaUserErrors;
if (errs.length) throw new Error(errs.map((e) => e.message).join("; "));
return data.productCreateMedia.media[0];
}
for await (const p of products()) {
const first = p.media.nodes.find((m) => m.image);
if (!first) continue;
const key = `shopify-${first.id}`;
const { url, credits_remaining } = await whiteBackground(first.image.url, key);
await attach(p.id, url, `${p.title} on white background`);
console.log(p.title, "→ attached; credits left", credits_remaining);
}Shopify fetches the image from the result URL asynchronously (status: "UPLOADED" then "READY"), well inside the 24-hour window. Reorder or delete the old media afterwards with productReorderMedia / productDeleteMedia once you have eyeballed a sample — keep the originals until then.
Dry run, cost and re-runs
Dry run first with size: "preview": it is free, returns an 800 px result, and lets you check edges on jewellery, glass and white products before spending a credit. A full run costs one credit per image — 500 SKUs is the $39 pack ($0.078 per image), 2,000 is $129. Credits never expire, so buy once for the catalogue. Because the idempotency key is the Shopify media id, re-running the script after a crash skips the charge for images already processed within 24 hours; beyond that, track processed ids in a JSON file so the loop can resume. If you sell on Amazon or Flipkart as well, the same JPG-on-white output meets their main-image requirement — see Amazon listing background remover.
Frequently asked questions
Do I need to install a Shopify app?
No — a custom app token with read_products and write_products is enough, and the script runs anywhere Node runs.
Will Shopify accept the temporary result URL?
Yes. productCreateMedia downloads from originalSource when you call it; the bgclear result URL stays valid for 24 hours, far longer than needed.
Does the white match Shopify's or Amazon's requirement?
bg_color=ffffff produces pure RGB 255,255,255, which is what marketplace main-image rules ask for. Keep a subtle drop shadow out of the main image.
What about variant images and Shopify Plus stores with thousands of products?
The same loop works over variant media; for very large catalogues submit 50 image_urls per call to POST /api/v1/jobs/batch with a callback URL instead of the synchronous endpoint.