> ## Documentation Index
> Fetch the complete documentation index at: /docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Download Amazon product images with Python

> Extract product and variant images with Python, handle cached responses and queued jobs, and download source files without exposing your API key.

Use AmzDownloader to collect one product's main and variant images, then save up to 20 unique image files with Python. This tutorial keeps the original media response so you can trace each image to its variant, marketplace and retrieval time.

## Prepare the example

Create a key on the [API keys page](https://amzdownloader.com/dashboard/api-keys). You need Python 3.9 or newer and permission to use the images you download. The runnable files use Python's standard library.

Clone the public documentation examples and set your key in the shell:

```bash theme={null}
git clone https://github.com/ecom-crawl/amzdownloader-api-docs.git
cd amzdownloader-api-docs
export AMZDOWNLOADER_API_KEY="YOUR_AMZDOWNLOADER_API_KEY"
export AMZDOWNLOADER_IDEMPOTENCY_KEY="images-my-product-001"
python3 examples/python/download_images.py \
  'https://www.amazon.com/dp/B0CRY1MG68' './amazon-images'
```

Use a new operation ID for different work. Keep the original ID if you deliberately retry the same URL and settings after losing a response. Do not print or commit your API key.

## Read the complete workflow

The example imports the bundled `client.py` helper. It sends one `POST /scrapes` with image groups enabled and video groups disabled. HTTP `200` means it already has completed media; HTTP `202` means it must poll the returned job ID. Polls are limited to 20, separated by three seconds. Each network request also has a timeout.

```python download_images.py theme={null}
"""Run: python3 examples/python/download_images.py PRODUCT_URL OUTPUT_DIRECTORY."""
import argparse
import json
import os
from pathlib import Path
import sys
import time
import urllib.parse
import urllib.request
from client import Client, NoRedirect, job_path

IMAGE_SELECTION = {
    "productImages": True, "variantImages": True,
    "productVideos": False, "relatedVideos": False, "creatorVideos": False,
}

def extract_images(client, url, operation_id, wait=time.sleep, max_polls=20):
    status, result = client.request("POST", "/scrapes", {
        "url": url, "renderMode": "auto", "include": IMAGE_SELECTION,
    }, operation_id)
    if status == 200 and result.get("status") == "completed":
        return result
    if status != 202:
        raise RuntimeError("Expected completed media or an extraction job")
    job_id = result.get("jobId")
    path = job_path(job_id, "/result")
    print("Save this job ID: " + job_id, file=sys.stderr)
    for _ in range(max_polls):
        wait(3)
        status, result = client.request("GET", path)
        if status == 200 and result.get("status") == "completed":
            return result
        if status != 202 or result.get("status") not in ("queued", "processing"):
            raise RuntimeError("Extraction stopped; inspect the saved job")
    raise RuntimeError("Polling limit reached; check saved job " + job_id)

def image_urls(media):
    values = list(media.get("productImages", []))
    for variant in media.get("variantImages", []):
        values.extend(variant.get("images", []))
    return list(dict.fromkeys(item["src"] for item in values if item.get("src")))

def download_image(url, directory, index, opener=None):
    parsed = urllib.parse.urlsplit(url)
    host = (parsed.hostname or "").lower()
    allowed = any(host == domain or host.endswith("." + domain) for domain in (
        "media-amazon.com", "ssl-images-amazon.com", "images-amazon.com",
    ))
    if parsed.scheme != "https" or parsed.username or parsed.password or parsed.port or not allowed:
        raise ValueError("Refusing an image URL outside Amazon media hosts")
    # This request carries no AmzDownloader authorization header.
    opener = opener or urllib.request.build_opener(NoRedirect())
    with opener.open(urllib.request.Request(url), timeout=30) as response:
        suffix = {"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp"}.get(response.headers.get_content_type())
        if not suffix:
            raise ValueError("Unsupported image content type")
        data = response.read(10 * 1024 * 1024 + 1)
        if len(data) > 10 * 1024 * 1024:
            raise ValueError("Image exceeds this example's 10 MB limit")
    target = directory / ("image-%03d" % index + suffix)
    with target.open("xb") as output:
        output.write(data)
    return target.name

def main():
    parser = argparse.ArgumentParser(description="Extract one product's images and download at most 20 unique source files")
    parser.add_argument("url")
    parser.add_argument("output_directory", type=Path)
    args = parser.parse_args()
    operation_id = os.environ.get("AMZDOWNLOADER_IDEMPOTENCY_KEY")
    if not operation_id:
        parser.error("Set AMZDOWNLOADER_IDEMPOTENCY_KEY to a stable ID for this operation")
    result = extract_images(Client(), args.url, operation_id)
    args.output_directory.mkdir(parents=True, exist_ok=True)
    (args.output_directory / "media-result.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
    data = result["data"]
    urls = image_urls(data["media"])
    saved = [download_image(url, args.output_directory, i + 1) for i, url in enumerate(urls[:20])]
    print(json.dumps({"asin": data["source"]["asin"], "availableImages": len(urls), "savedFiles": saved, "warnings": data["warnings"]}, indent=2))

if __name__ == "__main__":
    try:
        main()
    except Exception as error:
        sys.exit(str(error))
```

The image download requests are separate from the authenticated API requests. They accept Amazon media hosts, reject redirects and omit your API key. The 20-file and 10 MB per-image limits are safeguards in this example, not service-wide API limits. Existing image files are not overwritten.

## Inspect the result

The output directory contains `media-result.json` and the downloaded `image-001.jpg`, `image-002.png` or equivalent files. The suffix follows the response content type. Inspect these response fields:

| Field | Meaning |
| - | - |
| `data.media.productImages[].src` | Main product source image URL |
| `data.media.variantImages[].title` | Variant label reported by the source |
| `data.media.variantImages[].sku` | Variant identifier reported by the source |
| `data.media.variantImages[].images[].src` | Images associated with that variant |
| `data.source.fetchedAt` | When the media record was retrieved |
| `data.warnings` | Coverage and freshness limitations |

The script downloads each distinct URL once, even when multiple groups reference it. The complete grouped response remains in `media-result.json`; filenames do not imply a particular variant. If there are no images, inspect the warnings rather than interpreting the empty list as proof that Amazon has none.

## Resume safely and understand credits

A successful extraction costs one credit, including a cached result. Downloading the returned image URLs does not make another AmzDownloader API call. Polls, owned job result reads and identical idempotent replays cost no additional credits.

If the local polling budget ends, save the job ID printed to stderr and inspect it with `GET /scrapes/{jobId}` or `/scrapes/{jobId}/result`. A timeout does not cancel the job. If downloading a file fails after extraction, inspect the saved JSON before starting another charged request. Source URLs can expire; a newly requested [cached read](/docs/cache.md) is itself a one-credit media retrieval on success.

See the [image API overview](https://amzdownloader.com/api/amazon-image-downloader), [variant image API](https://amzdownloader.com/api/amazon-variant-images), [selection guide](/docs/extraction.md) and [errors and retries](/docs/errors.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.