# E-commerce Image Pack Pipeline (`plusonets/ecommerce-image-pack`) Actor

Turn product photos into white-background image packs sized for Amazon, Etsy, Shopify, and eBay.

- **URL**: https://apify.com/plusonets/ecommerce-image-pack.md
- **Developed by:** [Olalekan Johnson](https://apify.com/plusonets) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 image processeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## E-commerce Image Pack Pipeline

Turn a product photo into a marketplace-ready pack: background removed, placed on pure white, sized for Amazon, Etsy, Shopify, and eBay, compressed, and stripped of EXIF. One run, one ZIP, one report row per photo.

Unofficial, and not affiliated with Amazon, Etsy, Shopify, or eBay. Channel presets follow those companies' published image pages as checked on 23 October 2026. Category style guides can be stricter than the preset.

For sellers and listing agencies who repeat the same export every time a photo comes in.

- A ZIP (`IMAGE_PACK` in the key-value store) with one file per channel, plus `manifest.csv`.
- A dataset row per source photo: sizes, hashes, alt text, confidence, and whether it changed since the last run.
- A run summary (`OUTPUT`) with `rowsCharged`, `rowsDropped`, `fillRate`, and error counts.

**You're only charged for rows that pass validation.** A row is billed only when every channel you asked for produced a file that matches that channel's size, format, byte cap, and (where required) white background, and the file has no EXIF.

### What a row looks like

```json
{
  "status": "ok",
  "charged": true,
  "coverage": "complete",
  "fillRate": 1,
  "observedAt": "2026-10-23T16:30:00Z",
  "sourceTitle": "Coffee cup",
  "sourceKind": "sample",
  "channelsRequested": "amazon,etsy,shopify,ebay",
  "channelsPassed": "amazon,etsy,shopify,ebay",
  "backgroundRemoved": true,
  "whiteBackground": true,
  "exifStripped": true,
  "confidence": 0.9938,
  "altText": "Coffee cup. White-background product photo. Prepared for Amazon (2000×2000), Etsy (2000×2000), Shopify (2048×2048), and eBay (1600×1600).",
  "altTextSource": "deterministic",
  "changedSincePrevious": true,
  "amazonFile": "amazon/01-coffee-cup.jpg",
  "amazonWidth": 2000,
  "amazonHeight": 2000,
  "amazonBytes": 183417,
  "amazonFormat": "jpeg",
  "specVersion": "2026-10-23",
  "zipKey": "IMAGE_PACK"
}
```

Etsy, Shopify, and eBay use the same column pattern (`etsyFile`, `shopifyWidth`, `ebayBytes`, and so on). Failed fetches use `status` of `blocked`, `unsupported`, `failed`, or `no_results` and are not billed.

### Pricing

This Actor uses pay-per-event pricing. The event is `image-processed`: **$0.02 per validated source image**. There is no per-dataset-item charge. Apify also charges a $0.005 start event once per run.

Billing is per source image, not per channel file. Cutting the background out is the costly step and it runs once per photo. Exporting Amazon plus Etsy plus Shopify plus eBay after that is a resize. Charging once per photo keeps a 4-channel pack the same price as a 1-channel pack.

| Job | Events | Total |
| --- | --- | --- |
| 1 photo, any number of the four channels | 1 × $0.02 | $0.02 plus the $0.005 start |
| Default demo (3 public-domain photos, 4 channels) | 3 × $0.02 | $0.06 plus the $0.005 start |
| 100-photo catalog | 100 × $0.02 | $2.00 plus one start event |
| 1,000 photos | 1,000 × $0.02 | **$20** plus one start event |

1,000 results ≈ $20, plus one $0.005 start event for the run. `maxImages` defaults to 10 (maximum 50) so a dataset cannot run away. Raise it when you mean to.

Rows that fail validation are not charged. When the Actor runs without pay-per-event (for example locally), the rows are still delivered and `rowsCharged` stays 0. `fillRate` is still the share of finished rows that passed validation.

### How a photo is processed

1. Read URLs, an inline JSON or CSV list, a dataset, or the three bundled samples when the input is empty.
2. Reject private, loopback, link-local, and metadata addresses before the request. Redirects are checked again.
3. Download with a size cap. An HTTP 200 page that is HTML or a challenge is `blocked`, not a success.
4. Apply the EXIF orientation, then drop EXIF (including GPS) from the output. An sRGB profile is embedded so the file stays in sRGB.
5. Optionally cut the background out with U²-Net portable (`u2netp`), a 4.4 MB Apache-2.0 model, on CPU at 320×320. No GPU.
6. Place the product on pure white so its longer side is 85% of the channel canvas, then compress under that channel's byte cap.
7. Write alt text from the title and the channel sizes. An optional OpenAI key can phrase the sentence. If the key is missing or the call fails, the deterministic sentence is used.
8. Charge `image-processed` only after the row passes. Stop when the run's max total charge is reached. The same result id is not charged twice in one run.

### Channel presets

Checked 23 October 2026. `specVersion` on each row is `2026-10-23`.

| Channel | Canvas | File | What the publisher's page says |
| --- | --- | --- | --- |
| Amazon | 2000×2000 JPEG, pure white, product longer side at 85% of the frame, under 10,485,760 bytes | Main-image pack | [Seller Central help G1881](https://sellercentral.amazon.com/help/hub/reference/external/G1881) is login-gated. Amazon's own restatement of that page (Seller Central, checked the same day) says: longest side 500–10,000 px, at least 1,000 px for zoom, JPEG preferred (PNG, TIFF, or non-animated GIF also accepted), MAIN image on RGB 255,255,255, product shown as 85% of the image. The byte cap is the 10,485,760-byte ceiling shown in an Amazon upload dialog; G1881's public restatement specifies pixels and formats, not bytes. |
| Etsy | 2000×2000 JPEG, sRGB, under 960 KB | Edited companion image | [Etsy Help: image requirements](https://help.etsy.com/hc/en-us/articles/115015663347-Requirements-and-Best-Practices-for-Images-in-Your-Etsy-Shop). Listing photos are recommended at 2,000 px or more on width and height. The first photo should be at least 635 px. Files larger than 1 MB may fail to upload. Accepted types include JPEG and PNG. Transparent PNG is not supported. Square is allowed for the first photo. |
| Shopify | 2048×2048 JPEG, under 20 MB | Product image | [Product media types](https://help.shopify.com/en/manual/products/product-media/product-media-types). Square images usually display best at 2048×2048. Uploads can be up to 5000×5000 px or 25 megapixels and must be under 20 MB. JPEG is the recommended type for product photos. [File uploads](https://help.shopify.com/en/manual/shopify-admin/productivity-tools/file-uploads) also cite a 20 megapixel and 20 MB cap. 2048×2048 sits inside both. |
| eBay | 1600×1600 JPEG, under 7 MB | Listing photo | [Adding pictures (Australia)](https://www.ebay.com.au/help/selling/listings/adding-pictures-listings?id=4148): at least 500 px on the longest side, 800–1600 recommended, direct upload up to 7 MB (12 MB if imported from a URL). JPEG, PNG, GIF, TIFF, BMP, WEBP, HEIC, or AVIF. [UK Seller Centre](https://www.ebay.co.uk/sellercentre/listings/photo-tips): at least 1600×1600 recommended, and a white or neutral background. |

Two limits you should not skip:

- **Etsy still wants one unedited photo of the real item on a real background** for most listings. This pack is the edited white-background image, not that photo. See [how the requirement applies](https://help.etsy.com/hc/en-gb/articles/44297885802647-How-Do-Etsy-s-Listing-Image-Requirements-Apply-to-My-Listings).
- **Amazon category rules are not applied.** Apparel-on-a-model, a single shoe facing left, and similar rules live in category style guides. This preset is the ordinary main image: white, 85% fill, nothing cropped, no text added.

The 85% figure is the longer side of the product box as a fraction of the canvas. The product is not cropped to hit it. "Show the product as 85% of the image" is Amazon's wording; the other channels do not require that ratio. The same framing is used so the pack matches.

### Input

Leave every field blank and the run processes three CC0 sample photos (a coffee cup, a water bottle, a moulding bowl). That is the default, and it is what an empty `{}` input does.

| Field | Default | What it does |
| --- | --- | --- |
| `imageUrls` | empty | `https` image URLs, or a small `data:image/...;base64` URL. |
| `datasetId` | empty | Apify dataset to read. The run must be allowed to read it. |
| `datasetImageField` | `imageUrl` | Field that holds the URL. A list of URLs in that field works too. |
| `imagesJson` | empty | `["https://..."]` or `[{"url":"https://...","title":"Mug"}]`. |
| `imagesCsv` | empty | Header row with `url` and optional `title`. |
| `productTitle` | empty | Used when an image has no title of its own. Stored as plain text. |
| `channels` | all four | `amazon`, `etsy`, `shopify`, `ebay`. |
| `removeBackground` | true | CPU cutout. Turn off to only resize, pad, and strip EXIF. |
| `whiteBackground` | true | Flatten onto RGB 255,255,255. Amazon's preset does not export without it. |
| `outputFormat` | `jpeg` | `png` or `webp` when the channel allows it. Otherwise the file is JPEG. |
| `jpegQuality` | 90 | Starting quality. It steps down if a byte cap would be missed. |
| `suggestAltText` | true | Deterministic sentence. |
| `openAiApiKey` | empty secret | Optional. Your key, used only to phrase alt text. Not stored. |
| `maxImages` | 10 | Cap, maximum 50. |
| `maxImageBytes` | 8000000 | Reject larger downloads. |
| `alertOnlyOnChange` | false | Skip photos whose output hash matches the saved baseline. |

Private IPs, localhost, link-local and metadata hosts, credentials in the URL, and ports other than 80 and 443 are rejected. A public host that resolves to a private address is rejected too.

### Output

- Dataset: one row per source image. The Output tab has an **Image report** view and a **Channel files** view.
- Key-value `OUTPUT`: counts, `fillRate`, `errors` (`proxy`, `rate-limit`, `blocked`, `network`, `other`), seconds, and peak memory.
- Key-value `IMAGE_PACK`: the ZIP.
- Named key-value store `ecommerce-image-pack-state`, record `baseline`: hashes of billed outputs, used for the next run's diff. A run that throws does not replace this record. An unreadable baseline is left as it is.

`coverage` is `complete`, `partial`, `empty`, `blocked`, or `unsupported`. `partial` with `status: ok` still passed validation (usually a low-confidence mask or an upscale warning) and can be billed. `changedSincePrevious` is false when the file hash matches the baseline.

### Scheduled runs

Turn on `alertOnlyOnChange` for a daily task. Unchanged photos are not exported and not charged. If nothing changed, the dataset gets one unbilled row:

```json
{"status": "no_results", "charged": false, "coverage": "empty", "reason": "No channel files changed since the previous run."}
```

That is on purpose. A succeeded run with an empty dataset is a bad result for an agent. Duplicate bytes in the same run are processed once (`duplicateCount`). The same result id is not charged twice after a retry in the same run.

### Alt text

With no API key the sentence is built from the title and the channel sizes. It is one line of plain text.

If you set `openAiApiKey`, the Actor sends a small preview to OpenAI (`gpt-4o-mini`) and asks for a short description. The channel sizes are still appended here, not invented by the model. Any error, timeout, or unusable reply falls back to the deterministic sentence (`altTextSource` is `llm` or `deterministic`). The key is a secret input and is not written to the dataset or the log.

### Model and demo photos

Background removal uses U²-Net portable by Xuebin Qin et al., Apache License 2.0, which allows commercial use. The ONNX file is pinned at SHA-256 `309c8469258dda742793dce0ebea8e6dd393174f89934733ecc8b14c76f4ddd8`. See `THIRD_PARTY_NOTICES.md`. The model is small and runs on CPU. Edges on hair, glass, and cluttered backgrounds can be coarse. Read `confidence` before you publish the file. This is not a studio retouch.

Demo photos are resized CC0 images from Wikimedia Commons:

- Coffee cup, Pixel.la Free Stock Photos. https://commons.wikimedia.org/wiki/File:Coffee_cup_seen_from_above.jpg
- Water bottle, Jiafei Slay Queen. https://commons.wikimedia.org/wiki/File:Bottle_of_Water.jpg
- Moulding bowl, Kalakpagh. https://commons.wikimedia.org/wiki/File:Local_moulding_bowl.jpg

License: [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/).

### Integrations

- **Webhook.** On run succeeded, read the dataset items. Each item is one photo, flat fields, no nested objects.
- **n8n / Make.** Apify "Run Actor" then "Get dataset items". Download `IMAGE_PACK` from the key-value store when you need the files.
- **Google Sheets.** Apify's Google Sheets integration can append the dataset. The columns are already flat.
- **Another Actor.** Pass a `datasetId` from an upstream run and set `datasetImageField` to the URL field.

The Actor does not need Standby mode, a browser, or a proxy. Limited permissions are enough: it fetches the URLs you supply and, only if you set `datasetId`, reads that dataset.

### FAQ

**Why is a row `ok` but `charged` is false?**
Pay-per-event is not enabled yet, or the charge limit was already reached. The file can still be in the ZIP. `OUTPUT.billingActive` and `chargeLimitReached` say which.

**Why did Amazon get dropped when I turned white off?**
Amazon's main-image preset requires RGB 255,255,255. The other channels still export. If Amazon was the only channel, the row is not billed.

**Can I use the Etsy file as my only listing photo?**
No. Etsy asks most sellers for at least one unedited photo on a real background. Use this file as an additional image.

**Does it call a paid background API?**
No. The cutout runs locally. The only optional paid call is alt text, and only with your own key.

**What if the host returns a captcha?**
The row is `blocked` with `errorCategory: blocked` and is not charged. Timeouts and HTTP 5xx count as `network`. HTTP 429 counts as `rate-limit`.

### Run it locally

Python 3.12.

```bash
pip install -r requirements.txt
python -m ecommerce_image_pack
```

An empty input uses the sample photos and writes `storage/`. Set `ACTOR_TEST_PAY_PER_EVENT=1` if you want charge calls recorded locally. Tests:

```bash
pip install -r requirements-dev.txt
pytest
```

Contact: plusonecares@gmail.com

# Changelog

This Actor's version history is a separate document: https://apify.com/plusonets/ecommerce-image-pack/changelog.md

# Actor input Schema

## `imageUrls` (type: `array`):

Public http(s) image URLs, or small data:image/...;base64 URLs. Private addresses and localhost are rejected. Leave this empty to use the built-in sample photos.

## `datasetId` (type: `string`):

Optional Apify dataset to read. Each item should have a URL in datasetImageField. The run must be allowed to read that dataset.

## `datasetImageField` (type: `string`):

Item field that holds the image URL. A list of URL strings in that field is also accepted.

## `imagesJson` (type: `string`):

Optional JSON array of URLs or objects like {"url":"https://...","title":"Mug"}.

## `imagesCsv` (type: `string`):

Optional CSV text with a header row. Required column: url (or imageUrl). Optional column: title.

## `productTitle` (type: `string`):

Used in file names and alt text when an image has no title of its own. Treated as plain text.

## `channels` (type: `array`):

Marketplace presets to export. One source image produces one file per channel. You are billed per source image, not per channel.

## `removeBackground` (type: `boolean`):

Cut the product out with the small U²-Net model on CPU, then place it on the canvas.

## `whiteBackground` (type: `boolean`):

Flatten onto pure white RGB 255,255,255. Required for the Amazon main-image preset.

## `outputFormat` (type: `string`):

jpeg, png, or webp. A channel that does not accept the choice is saved as JPEG instead.

## `jpegQuality` (type: `integer`):

Starting quality, from 60 to 95. The encoder steps down if a channel file-size cap would be missed.

## `suggestAltText` (type: `boolean`):

Write alt text from the title and the channel sizes. A bring-your-own OpenAI key can phrase the sentence; if the call fails, the deterministic sentence is used.

## `openAiApiKey` (type: `string`):

Optional. Your own OpenAI key, used only to phrase alt text. It is not stored. Leave it empty to keep the deterministic sentence.

## `maxImages` (type: `integer`):

Cap on source images for this run. The default is 10 so a dataset or URL list cannot run away. Maximum 50. The empty-input demo uses three sample photos, which is under this cap.

## `maxImageBytes` (type: `integer`):

Reject a download larger than this many bytes before it is decoded.

## `alertOnlyOnChange` (type: `boolean`):

When scheduled, skip images whose output hash matches the saved baseline. If nothing changed, the dataset gets one unbilled no_results row.

## Actor input object example

```json
{
  "imageUrls": [],
  "datasetId": "",
  "datasetImageField": "imageUrl",
  "imagesJson": "",
  "imagesCsv": "",
  "productTitle": "",
  "channels": [
    "amazon",
    "etsy",
    "shopify",
    "ebay"
  ],
  "removeBackground": true,
  "whiteBackground": true,
  "outputFormat": "jpeg",
  "jpegQuality": 90,
  "suggestAltText": true,
  "maxImages": 10,
  "maxImageBytes": 8000000,
  "alertOnlyOnChange": false
}
```

# Actor output Schema

## `report` (type: `string`):

One row per source image. Charged rows have status ok and charged true.

## `summary` (type: `string`):

Counts, fill rate, error categories, and the ZIP key.

## `imagePack` (type: `string`):

ZIP of the channel files plus manifest.csv.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "imageUrls": [],
    "datasetId": "",
    "datasetImageField": "imageUrl",
    "imagesJson": "",
    "imagesCsv": "",
    "productTitle": "",
    "channels": [
        "amazon",
        "etsy",
        "shopify",
        "ebay"
    ],
    "removeBackground": true,
    "whiteBackground": true,
    "outputFormat": "jpeg",
    "jpegQuality": 90,
    "suggestAltText": true,
    "maxImages": 10,
    "maxImageBytes": 8000000,
    "alertOnlyOnChange": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("plusonets/ecommerce-image-pack").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "imageUrls": [],
    "datasetId": "",
    "datasetImageField": "imageUrl",
    "imagesJson": "",
    "imagesCsv": "",
    "productTitle": "",
    "channels": [
        "amazon",
        "etsy",
        "shopify",
        "ebay",
    ],
    "removeBackground": True,
    "whiteBackground": True,
    "outputFormat": "jpeg",
    "jpegQuality": 90,
    "suggestAltText": True,
    "maxImages": 10,
    "maxImageBytes": 8000000,
    "alertOnlyOnChange": False,
}

# Run the Actor and wait for it to finish
run = client.actor("plusonets/ecommerce-image-pack").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "imageUrls": [],
  "datasetId": "",
  "datasetImageField": "imageUrl",
  "imagesJson": "",
  "imagesCsv": "",
  "productTitle": "",
  "channels": [
    "amazon",
    "etsy",
    "shopify",
    "ebay"
  ],
  "removeBackground": true,
  "whiteBackground": true,
  "outputFormat": "jpeg",
  "jpegQuality": 90,
  "suggestAltText": true,
  "maxImages": 10,
  "maxImageBytes": 8000000,
  "alertOnlyOnChange": false
}' |
apify call plusonets/ecommerce-image-pack --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plusonets/ecommerce-image-pack"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/0yg8KygsBunBGBYfj/builds/MgZrwlWOxnxdUrF5b/openapi.json
