# Shopify Variant Image Association Audit (`practical_ai_lab_jp/pal-shopify-variant-image-association-audit`) Actor

Read-only audit of Shopify catalog snapshots: broken variant-image links, duplicate IDs and SKUs, missing alt text, and drift between two exports. Returns SKU-linked corrections.

- **URL**: https://apify.com/practical_ai_lab_jp/pal-shopify-variant-image-association-audit.md
- **Developed by:** [Practical AI Lab](https://apify.com/practical_ai_lab_jp) (community)
- **Categories:** E-commerce, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 snapshot audits

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

## Shopify Variant Image Association Audit

A read-only audit for Shopify catalog snapshots. It finds broken product-variant-image links, duplicate identifiers, missing or weak image alt text, and drift between two exports, then returns one row per problem with the affected SKU and a suggested correction. It never edits a storefront.

### Who it is for

Teams that migrate, merge, or bulk-edit Shopify catalogs and need to know what broke: store operators, catalog managers, and agencies reviewing a client export they are authorized to audit.

### What it checks

| Category | Issue types |
| --- | --- |
| Variant-image links | `VARIANT_IMAGE_NOT_IN_PRODUCT`, `VARIANT_IMAGE_FROM_OTHER_PRODUCT`, `IMAGE_VARIANT_LIST_MISMATCH`, `IMAGE_REFERENCES_UNKNOWN_VARIANT`, `VARIANT_PRODUCT_ID_MISMATCH`, `IMAGE_PRODUCT_ID_MISMATCH`, optional `VARIANT_WITHOUT_IMAGE` |
| Identifiers | `DUPLICATE_PRODUCT_ID`, `DUPLICATE_HANDLE`, `DUPLICATE_VARIANT_ID`, `DUPLICATE_SKU` (case-insensitive), `DUPLICATE_BARCODE`, `DUPLICATE_IMAGE_ID`, `MISSING_SKU` |
| Alt text | `MISSING_ALT_TEXT`, `ALT_TEXT_TOO_LONG`, `ALT_TEXT_LOOKS_LIKE_FILENAME` |
| Drift (needs a baseline) | `DRIFT_SKU_CHANGED`, `DRIFT_VARIANT_IMAGE_CHANGED`, `DRIFT_VARIANT_IMAGE_REMOVED`, `DRIFT_VARIANT_IMAGE_ADDED`, `DRIFT_VARIANT_REMOVED`, `DRIFT_VARIANT_ADDED`, `DRIFT_ALT_TEXT_REMOVED`, `DRIFT_ALT_TEXT_CHANGED` |

### Input

Provide the current snapshot either as a pasted `products` array (Shopify `products.json` shape) or as a public feed URL. Do not provide both for the same snapshot. The input form is prefilled with a small sample (current and baseline) that runs without any network access; clear those arrays before using a URL.

Copyable sample:

```json
{
  "currentProducts": [
    {
      "id": 1001, "title": "Trail Jacket", "handle": "trail-jacket",
      "variants": [
        {"id": 2001, "product_id": 1001, "title": "Blue / S", "sku": "TJ-BLU-S", "image_id": 3001},
        {"id": 2002, "product_id": 1001, "title": "Blue / M", "sku": "TJ-BLU-S", "image_id": 3999}
      ],
      "images": [
        {"id": 3001, "product_id": 1001, "src": "https://cdn.example.com/trail-jacket-blue.jpg", "alt": "", "variant_ids": [2001]}
      ]
    }
  ],
  "checkAltText": true
}
```

Main options: `currentProducts` / `currentSnapshotUrl`, `baselineProducts` / `baselineSnapshotUrl`, `checkAltText`, `maxAltLength`, `flagVariantsWithoutImage`, `maxProducts`, `maxPages`, `requestDelaySecs`, `maxFindings`.

#### URL snapshots

- Only `https://<host>/products.json` and `https://<host>/collections/<handle>/products.json` are accepted. Query strings are discarded.
- IP addresses, local or internal host names, credentials in URLs, and non-default ports are rejected. Hosts resolving to non-public addresses are rejected.
- `robots.txt` is read first. If it disallows the feed or cannot be read (other than 404), the run stops without fetching the feed.
- Redirects are not followed. Requests are paged (250 products per page), delayed, and bounded by `maxPages`, `maxProducts`, and a per-response size limit.
- Audit only stores you own or are authorized to review.

### Output

- **Dataset**: one row per finding, sorted by severity (`high`, `medium`, `low`). Key fields: `issueType`, `sku`, `productHandle`, `variantId`, `imageId`, `detail`, `suggestedAction`, `suggestedField`, `currentValue`, `suggestedValue`, `confidence`. `storefrontEdited` is always `false`.
- **Key-value store `SUMMARY`**: snapshot sizes, counts by severity, category and issue type, SKUs with the most findings, whether the findings cap truncated results, and the caveats below.

`confidence: "high"` is used only when exactly one image in the product explicitly lists the variant. Everything else is `review`.

### Pricing

The code defines a single pay-per-event name, `snapshot-audit`, charged once after an audit has finished and its results are stored. Runs that fail (invalid input, unsafe URL, robots refusal, fetch error, empty snapshot) are not charged. No price is configured in this project.

### Limitations and sample-size caveats

- The audit only sees what the snapshot contains. Public `products.json` feeds list published products only and may omit fields that a full admin export has, so a clean public result does not prove the whole catalog is clean.
- Inline arrays larger than `maxProducts` are truncated; URL feeds stop at `maxPages` or `maxProducts`. The run summary flags truncation.
- Image drift matches variants by id and images by file name. Re-uploaded images with new file names show up as changes.
- A variant whose image has an empty `variant_ids` list is not flagged as a list mismatch, because partial exports often omit that list.
- Suggested alt text is a draft assembled from product and variant titles. It needs human review and is not a description of the picture.
- Small snapshots, such as a handful of products, say little about catalog-wide health. Percentages from them should not be extrapolated.
- DNS is checked before fetching, but the HTTP client resolves the host again; a hostile DNS server could in theory change its answer between the two steps.
- No images are downloaded or analyzed; broken image URLs are not detected.
- Shopify's JSON shape can change. Unknown or malformed entries are skipped rather than repaired.

### Local tests

Tests run offline with the standard library:

```
python -m unittest discover
```

# Actor input Schema

## `currentProducts` (type: `array`):

Products in Shopify products.json shape (id, handle, title, variants\[], images\[]). Use this for exports you are authorized to audit. Leave empty if you use a URL.

## `currentSnapshotUrl` (type: `string`):

HTTPS URL of a public Shopify feed, for example https://store.example.com/products.json or https://store.example.com/collections/sale/products.json. Only audit stores you own or are authorized to review. robots.txt is checked first.

## `baselineProducts` (type: `array`):

An earlier export in the same shape. When provided, the Actor reports drift in SKUs, variant-image links and alt text.

## `baselineSnapshotUrl` (type: `string`):

Public HTTPS feed URL of an earlier or alternative catalog to compare against. Rarely useful for live stores, since a live feed shows only the current state.

## `checkAltText` (type: `boolean`):

Report missing, over-long and filename-like image alt text.

## `maxAltLength` (type: `integer`):

Alt text longer than this is flagged.

## `flagVariantsWithoutImage` (type: `boolean`):

Report variants of multi-variant products that have no image assigned. Off by default because many stores intentionally share one image.

## `maxProducts` (type: `integer`):

Upper bound on products read from each snapshot.

## `maxPages` (type: `integer`):

Each page holds up to 250 products. Applies to URL snapshots only.

## `requestDelaySecs` (type: `number`):

Pause between page requests to a store. Minimum 0.5.

## `maxFindings` (type: `integer`):

Findings are sorted by severity before this cap is applied. The summary reports the uncapped total.

## Actor input object example

```json
{
  "currentProducts": [
    {
      "id": 1001,
      "title": "Trail Jacket",
      "handle": "trail-jacket",
      "variants": [
        {
          "id": 2001,
          "product_id": 1001,
          "title": "Blue / S",
          "sku": "TJ-BLU-S",
          "image_id": 3001
        },
        {
          "id": 2002,
          "product_id": 1001,
          "title": "Blue / M",
          "sku": "TJ-BLU-S",
          "image_id": 3999
        }
      ],
      "images": [
        {
          "id": 3001,
          "product_id": 1001,
          "src": "https://cdn.example.com/trail-jacket-blue.jpg",
          "alt": "",
          "variant_ids": [
            2001
          ]
        }
      ]
    }
  ],
  "baselineProducts": [
    {
      "id": 1001,
      "title": "Trail Jacket",
      "handle": "trail-jacket",
      "variants": [
        {
          "id": 2001,
          "product_id": 1001,
          "title": "Blue / S",
          "sku": "TJ-BLU-S",
          "image_id": 3001
        },
        {
          "id": 2002,
          "product_id": 1001,
          "title": "Blue / M",
          "sku": "TJ-BLU-M",
          "image_id": 3002
        }
      ],
      "images": [
        {
          "id": 3001,
          "product_id": 1001,
          "src": "https://cdn.example.com/trail-jacket-blue.jpg",
          "alt": "Blue trail jacket, front view",
          "variant_ids": [
            2001
          ]
        },
        {
          "id": 3002,
          "product_id": 1001,
          "src": "https://cdn.example.com/trail-jacket-blue-m.jpg",
          "alt": "Blue trail jacket, size M",
          "variant_ids": [
            2002
          ]
        }
      ]
    }
  ],
  "checkAltText": true,
  "maxAltLength": 512,
  "flagVariantsWithoutImage": false,
  "maxProducts": 2000,
  "maxPages": 10,
  "requestDelaySecs": 1,
  "maxFindings": 5000
}
```

# Actor output Schema

## `results` (type: `string`):

Download rows from the latest run in JSON/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 = {
    "currentProducts": [
        {
            "id": 1001,
            "title": "Trail Jacket",
            "handle": "trail-jacket",
            "variants": [
                {
                    "id": 2001,
                    "product_id": 1001,
                    "title": "Blue / S",
                    "sku": "TJ-BLU-S",
                    "image_id": 3001
                },
                {
                    "id": 2002,
                    "product_id": 1001,
                    "title": "Blue / M",
                    "sku": "TJ-BLU-S",
                    "image_id": 3999
                }
            ],
            "images": [
                {
                    "id": 3001,
                    "product_id": 1001,
                    "src": "https://cdn.example.com/trail-jacket-blue.jpg",
                    "alt": "",
                    "variant_ids": [
                        2001
                    ]
                }
            ]
        }
    ],
    "baselineProducts": [
        {
            "id": 1001,
            "title": "Trail Jacket",
            "handle": "trail-jacket",
            "variants": [
                {
                    "id": 2001,
                    "product_id": 1001,
                    "title": "Blue / S",
                    "sku": "TJ-BLU-S",
                    "image_id": 3001
                },
                {
                    "id": 2002,
                    "product_id": 1001,
                    "title": "Blue / M",
                    "sku": "TJ-BLU-M",
                    "image_id": 3002
                }
            ],
            "images": [
                {
                    "id": 3001,
                    "product_id": 1001,
                    "src": "https://cdn.example.com/trail-jacket-blue.jpg",
                    "alt": "Blue trail jacket, front view",
                    "variant_ids": [
                        2001
                    ]
                },
                {
                    "id": 3002,
                    "product_id": 1001,
                    "src": "https://cdn.example.com/trail-jacket-blue-m.jpg",
                    "alt": "Blue trail jacket, size M",
                    "variant_ids": [
                        2002
                    ]
                }
            ]
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("practical_ai_lab_jp/pal-shopify-variant-image-association-audit").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 = {
    "currentProducts": [{
            "id": 1001,
            "title": "Trail Jacket",
            "handle": "trail-jacket",
            "variants": [
                {
                    "id": 2001,
                    "product_id": 1001,
                    "title": "Blue / S",
                    "sku": "TJ-BLU-S",
                    "image_id": 3001,
                },
                {
                    "id": 2002,
                    "product_id": 1001,
                    "title": "Blue / M",
                    "sku": "TJ-BLU-S",
                    "image_id": 3999,
                },
            ],
            "images": [{
                    "id": 3001,
                    "product_id": 1001,
                    "src": "https://cdn.example.com/trail-jacket-blue.jpg",
                    "alt": "",
                    "variant_ids": [2001],
                }],
        }],
    "baselineProducts": [{
            "id": 1001,
            "title": "Trail Jacket",
            "handle": "trail-jacket",
            "variants": [
                {
                    "id": 2001,
                    "product_id": 1001,
                    "title": "Blue / S",
                    "sku": "TJ-BLU-S",
                    "image_id": 3001,
                },
                {
                    "id": 2002,
                    "product_id": 1001,
                    "title": "Blue / M",
                    "sku": "TJ-BLU-M",
                    "image_id": 3002,
                },
            ],
            "images": [
                {
                    "id": 3001,
                    "product_id": 1001,
                    "src": "https://cdn.example.com/trail-jacket-blue.jpg",
                    "alt": "Blue trail jacket, front view",
                    "variant_ids": [2001],
                },
                {
                    "id": 3002,
                    "product_id": 1001,
                    "src": "https://cdn.example.com/trail-jacket-blue-m.jpg",
                    "alt": "Blue trail jacket, size M",
                    "variant_ids": [2002],
                },
            ],
        }],
}

# Run the Actor and wait for it to finish
run = client.actor("practical_ai_lab_jp/pal-shopify-variant-image-association-audit").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 '{
  "currentProducts": [
    {
      "id": 1001,
      "title": "Trail Jacket",
      "handle": "trail-jacket",
      "variants": [
        {
          "id": 2001,
          "product_id": 1001,
          "title": "Blue / S",
          "sku": "TJ-BLU-S",
          "image_id": 3001
        },
        {
          "id": 2002,
          "product_id": 1001,
          "title": "Blue / M",
          "sku": "TJ-BLU-S",
          "image_id": 3999
        }
      ],
      "images": [
        {
          "id": 3001,
          "product_id": 1001,
          "src": "https://cdn.example.com/trail-jacket-blue.jpg",
          "alt": "",
          "variant_ids": [
            2001
          ]
        }
      ]
    }
  ],
  "baselineProducts": [
    {
      "id": 1001,
      "title": "Trail Jacket",
      "handle": "trail-jacket",
      "variants": [
        {
          "id": 2001,
          "product_id": 1001,
          "title": "Blue / S",
          "sku": "TJ-BLU-S",
          "image_id": 3001
        },
        {
          "id": 2002,
          "product_id": 1001,
          "title": "Blue / M",
          "sku": "TJ-BLU-M",
          "image_id": 3002
        }
      ],
      "images": [
        {
          "id": 3001,
          "product_id": 1001,
          "src": "https://cdn.example.com/trail-jacket-blue.jpg",
          "alt": "Blue trail jacket, front view",
          "variant_ids": [
            2001
          ]
        },
        {
          "id": 3002,
          "product_id": 1001,
          "src": "https://cdn.example.com/trail-jacket-blue-m.jpg",
          "alt": "Blue trail jacket, size M",
          "variant_ids": [
            2002
          ]
        }
      ]
    }
  ]
}' |
apify call practical_ai_lab_jp/pal-shopify-variant-image-association-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,practical_ai_lab_jp/pal-shopify-variant-image-association-audit"
        }
    }
}
```

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/fBhnYguRm0PeZuXQR/builds/wGY11P0gd5ZXtpgQ3/openapi.json
