# Shopify Variant Price & Availability Checker (`slatey_sea/my-actor`) Actor

Compare Shopify variant prices and availability with your expected values. Extract SKUs and flag mismatches only when country and currency match. Includes Python and n8n examples. $2/1,000 verified products; unverified results have no event fee.

- **URL**: https://apify.com/slatey_sea/my-actor.md
- **Developed by:** [İBRAHİM ÇAMYURDU](https://apify.com/slatey_sea) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 verified products

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 Price Checker API: SKUs, Prices and Availability

Does a Shopify product variant still match the price in your catalog or automation?
Submit a public product URL and an exact variant ID to compare its published price and purchasability with your expected values. The result is **MATCH**, **MISMATCH**, or **UNVERIFIED**, with the country, currency, observation time and source evidence.

**$2 per 1,000 fully verified input products. No startup fee or per-variant fee. Unverified and incomplete inputs have no custom event fee.**

### When to use this Shopify price API

- Check a known variant against a price in your catalog before an external catalog update.
- Extract variant IDs, SKUs, prices and published availability from a supported product page.
- Add a product-price check to a Python or n8n workflow.

The Actor checks the values you supply. It does not discover competing shops, detect changes between previous runs, or send alerts by itself. A caller can store snapshots and compare them separately.

### Try one product

For variant discovery, use:

```json
{"items":[{"url":"https://www.andar.com/products/the-getaway"}],"maxResults":100}
```

For an exact expected-price check, use:

```json
{
  "items": [
    {
      "url": "https://www.andar.com/products/the-getaway",
      "variantId": "42860017483858",
      "expectedPrice": "118.00",
      "expectedCurrency": "USD",
      "expectedCountry": "US"
    }
  ],
  "maxResults": 100
}
```

This is a real product and variant used in our tests. **118.00 USD was observed on November 4, 2026; it is not a promise of the current price.** Change the expected value to the value from your own catalog. Use Apify Console's maximum cost per run to control spending; the API examples below cap a run at $0.01.

### Recorded real examples: match, mismatch and unverified market

These are saved cloud observations from November 4, 2026, not live quotes or customer testimonials.

| Case | Observed storefront price | Supplied expectation | Outcome |
| --- | --- | --- | --- |
| Andar, variant `42860017483858` | 118.00 USD, US | 118.00 USD, US | MATCH |
| Same Andar variant | 118.00 USD, US | Deliberately changed to 118.01 USD, US | MISMATCH |
| POPFLEX XXS, variant `40143986720851` | 60.00 USD, US | 66.00 USD, TR | UNVERIFIED |

The second case is a controlled comparison, **not a claim that the merchant made a pricing error**. In the third case, USD alone does not establish the market: the requested country differs, so the Actor does not compare price or availability and does not charge a verified-product event.

Example output, reduced to the relevant fields:

```json
[
  {
    "inputIndex": 0,
    "productTitle": "The Getaway",
    "variantId": "42860017483858",
    "price": "118.00",
    "currency": "USD",
    "observedCountry": "US",
    "expectedCountry": "US",
    "status": "MATCH",
    "checks": {
      "price": "MATCH",
      "purchasable": "MATCH"
    },
    "reasons": [],
    "observedAt": "2026-11-04T16:42:56.329163+00:00"
  },
  {
    "inputIndex": 1,
    "productTitle": "The Getaway",
    "variantId": "42860017483858",
    "price": "118.00",
    "currency": "USD",
    "observedCountry": "US",
    "expectedCountry": "US",
    "status": "MISMATCH",
    "checks": {
      "price": "MISMATCH"
    },
    "reasons": [],
    "observedAt": "2026-11-04T16:42:56.329237+00:00"
  },
  {
    "inputIndex": 14,
    "productTitle": "Pirouette Skort - Black",
    "variantId": "40143986720851",
    "price": "60.00",
    "currency": "USD",
    "observedCountry": "US",
    "expectedCountry": "TR",
    "status": "UNVERIFIED",
    "checks": {
      "market": "UNVERIFIED"
    },
    "reasons": [
      "Target country is missing, differs, or cannot be established; price and availability were not compared"
    ],
    "observedAt": "2026-11-04T16:43:05.721236+00:00"
  }
]
```

### What the result means

| Status | Meaning | Custom product event fee |
| --- | --- | --- |
| EXTRACTED | Requested variants were extracted with supported currency and observed country | $0.002 per complete input product |
| MATCH | The supplied expectations match in the verified country and currency | $0.002 per complete input product |
| MISMATCH | At least one supplied expectation differs in that same verified market | $0.002 per complete input product |
| UNVERIFIED | Blocked, unsupported, missing, conflicting or different-market data | None |

An input product with multiple returned variants incurs one product event only if all requested variants are fully delivered and verified. Repeating the same product as separate input entries can incur separate product events. Truncated inputs have no custom product fee.

### Input and limits

- `items`: 1–100 objects containing a direct public HTTPS `/products/handle` URL; an optional locale prefix is supported.
- `variantId`: an exact numeric variant ID, or use `?variant=ID` in the URL. Omit it to request all returned variants.
- `expectedPrice`: a nonnegative decimal string with at most two decimal places; requires `expectedCurrency`.
- `expectedCurrency`: a supported uppercase currency code.
- `expectedCountry`: an uppercase two-letter country code. Every expected-value comparison requires this and an exact variant ID.
- `expectedPurchasable`: optional boolean for the store's published availability flag.
- `maxResults`: 1–5,000 output rows; default 1,000. Check the summary for truncation and unprocessed inputs.

Supported currencies: USD, EUR, GBP, TRY, CAD, AUD, NZD, CHF, SEK, NOK, DKK and PLN.

`expectedCountry` validates the country actually observed. **It does not change the storefront country, move the server, or force a currency.** Missing or conflicting country information, or a different country, produces UNVERIFIED for comparisons. Use the final canonical product URL; country/currency query parameters are rejected rather than silently removed.

Ajax fallback data must also match an exact-variant JSON-LD price, currency and availability in the product HTML. Headless or protected stores and custom pricing systems may be unsupported. HTTP 403/429 stops further requests to that host within a processing batch; paid runs process small batches and do not rotate proxies or retry blocked requests within a batch.

Purchasability is the merchant's published flag, not a physical stock count or delivery promise. Checkout-only discounts, tax, shipping and subscription prices are outside scope. This is not a Google Merchant Center approval check. This independent community Actor is not affiliated with Shopify or the example merchants.

### Python: bounded run, polling and saved diagnostics

Save the script below as `check_prices.py`. It uses the Python standard library.

1. `python check_prices.py` prints an offline preview and makes no API call.
2. Set `APIFY_TOKEN` in your local environment. Never put a token in a URL or shared file.
3. `python check_prices.py --run --max-cost-usd 0.01` starts one run and writes `price-results.json`.
4. To retrieve the same run again, use `python check_prices.py --resume RUN_ID`. Do not restart a POST after an ambiguous network timeout; find the existing run in Apify Console first.

The script retrieves both variant results and SUMMARY, including incomplete runs. An unverified row remains unverified; a budget stop is not silently presented as a complete audit.

```python
"""Apify price-check example. Defaults to an offline preview; --run starts a billed run."""
import argparse
import json
import os
import re
import time
from decimal import Decimal
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import Request, urlopen

ACTOR_ID = '73ZJkvQzoJ7oxQh99'
BASE = 'https://api.apify.com/v2'
INPUT = {'items': [{
    'url': 'https://www.andar.com/products/the-getaway',
    'variantId': '42860017483858', 'expectedPrice': '118.00',
    'expectedCurrency': 'USD', 'expectedCountry': 'US',
}], 'maxResults': 100}
TERMINAL = {'SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT'}


def identifier(value):
    if not isinstance(value, str) or not re.fullmatch(r'[A-Za-z0-9]+', value):
        raise ValueError('Invalid Apify resource ID')
    return value


def api(token, method, path, data=None):
    request = Request(BASE + path, method=method,
                      headers={'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'},
                      data=None if data is None else json.dumps(data).encode())
    with urlopen(request, timeout=75) as response:
        return json.load(response)


def collect(token, run):
    run_id = identifier(run['id'])
    deadline = time.monotonic() + 240
    while run['status'] not in TERMINAL:
        if time.monotonic() >= deadline:
            raise TimeoutError(f'Run {run_id} is still active. Resume this ID; do not start it again.')
        time.sleep(3)
        run = api(token, 'GET', f'/actor-runs/{run_id}?waitForFinish=10')['data']
    # Collect diagnostics even when the Actor correctly failed an all-unverified run.
    store = identifier(run['defaultKeyValueStoreId'])
    dataset = identifier(run['defaultDatasetId'])
    summary = api(token, 'GET', f'/key-value-stores/{store}/records/SUMMARY')
    rows = api(token, 'GET', f'/datasets/{dataset}/items?format=json&clean=true&limit=5000')
    complete = (run['status'] == 'SUCCEEDED' and summary.get('unprocessedInputCount') == 0
                and not summary.get('truncatedInputIndices') and not summary.get('stoppedByBudget'))
    return {'runId': run_id, 'runStatus': run['status'], 'complete': complete,
            'summary': summary, 'results': rows}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--run', action='store_true', help='Start one run; may consume Apify credit')
    parser.add_argument('--resume', help='Fetch an existing run instead of starting another')
    parser.add_argument('--input', type=Path)
    parser.add_argument('--max-cost-usd', default='0.01')
    parser.add_argument('--output', type=Path, default=Path('price-results.json'))
    args = parser.parse_args()
    if args.run and args.resume:
        parser.error('Choose --run or --resume, not both')
    amount = Decimal(args.max_cost_usd)
    if not amount.is_finite() or not Decimal('0.002') <= amount <= Decimal('0.20'):
        parser.error('Use a budget between $0.002 and $0.20 for this example')
    payload = json.loads(args.input.read_text(encoding='utf-8-sig')) if args.input else INPUT
    if not args.run and not args.resume:
        print(json.dumps({'mode': 'OFFLINE_PREVIEW', 'maxCostUsd': str(amount), 'input': payload}, indent=2))
        return
    token = os.environ.get('APIFY_TOKEN')
    if not token:
        parser.error('Set APIFY_TOKEN in your environment; never paste it into the workflow or a URL')
    if args.resume:
        run = api(token, 'GET', '/actor-runs/' + identifier(args.resume))['data']
    else:
        query = urlencode({'maxTotalChargeUsd': str(amount), 'memory': 256, 'timeout': 180,
                           'restartOnError': 'false', 'waitForFinish': 60})
        # Do not retry this POST automatically: a network timeout may follow a successful start.
        run = api(token, 'POST', f'/acts/{ACTOR_ID}/runs?{query}', payload)['data']
    print('Run ID:', run['id'])
    report = collect(token, run)
    args.output.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding='utf-8')
    print('Saved:', args.output, '| Complete:', report['complete'])
    if not report['complete']:
        raise SystemExit('Incomplete or unsuccessful run. Inspect the saved summary and diagnostics.')


if __name__ == '__main__':
    main()

```

API reference: [Start an Actor run](https://docs.apify.com/api/v2/actors-runs-post) and [Get a run](https://docs.apify.com/api/v2/actor-run-get).

### n8n: manually triggered workflow

The starter below runs one input, checks the run state and completeness, then returns both the summary and variant rows. Import the JSON into n8n with its workflow import feature.

1. Create an **HTTP Header Auth** credential: header name `Authorization`, value `Bearer YOUR_APIFY_TOKEN`.
2. Select that credential in **Start run**, **Run summary** and **Fetch variants**. This export contains no token or credential ID.
3. Edit the JSON body in **Start run** for your products. The URL sets `maxTotalChargeUsd=0.01`, 256 MB memory and a 180-second Actor timeout.
4. Execute manually when ready. Running it can consume your Apify credit. There is no schedule and the workflow is inactive on import.

This minimal workflow waits up to 60 seconds for the Actor. If it is still running, **Check run** stops with its run ID instead of starting a duplicate. Retrieve that existing run after it finishes. For longer jobs, extend the workflow with Wait + Get run polling. The Python example already polls. Run failure and incomplete summaries stop the workflow; UNVERIFIED diagnostics in an otherwise complete run remain in the output.

The JSON and Code-node guards were checked offline. An end-to-end execution inside an n8n installation has not been performed.

```json
{
  "name": "Shopify variant price checks - manual starter",
  "nodes": [
    {
      "id": "manual-run",
      "name": "Manual run",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "start-run",
      "name": "Start run",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/73ZJkvQzoJ7oxQh99/runs?waitForFinish=60&maxTotalChargeUsd=0.01&memory=256&timeout=180&restartOnError=false",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 75000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\"items\": [{\"url\": \"https://www.andar.com/products/the-getaway\", \"variantId\": \"42860017483858\", \"expectedPrice\": \"118.00\", \"expectedCurrency\": \"USD\", \"expectedCountry\": \"US\"}], \"maxResults\": 100}"
      },
      "retryOnFail": false,
      "notes": "Select your own Header Auth credential: Authorization = Bearer YOUR_APIFY_TOKEN. Do not paste a token into this export.",
      "notesInFlow": true
    },
    {
      "id": "check-run",
      "name": "Check run",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ],
      "parameters": {
        "jsCode": "const run = $input.first().json.data;\nif (!run || run.status !== 'SUCCEEDED') {\n  throw new Error(`Run ${run?.id || 'unknown'} is ${run?.status || 'unknown'}. Open this run in Apify Console and resume retrieval when finished. Do not repeat the Start run node: that starts a new potentially billed run.`);\n}\nreturn [{json: run}];"
      }
    },
    {
      "id": "run-summary",
      "name": "Run summary",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.apify.com/v2/key-value-stores/\" + $(\"Check run\").first().json.defaultKeyValueStoreId + \"/records/SUMMARY\" }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 75000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": false,
      "notes": "Select your own Header Auth credential: Authorization = Bearer YOUR_APIFY_TOKEN. Do not paste a token into this export.",
      "notesInFlow": true
    },
    {
      "id": "check-completeness",
      "name": "Check completeness",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ],
      "parameters": {
        "jsCode": "const summary = $input.first().json;\nif (summary.unprocessedInputCount !== 0 || summary.stoppedByBudget || !Array.isArray(summary.truncatedInputIndices) || summary.truncatedInputIndices.length) {\n  throw new Error('Incomplete result: budget or output limit was reached. Inspect the Run summary node and the existing Apify run.');\n}\nreturn [{json: summary}];"
      }
    },
    {
      "id": "fetch-variants",
      "name": "Fetch variants",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.apify.com/v2/datasets/\" + $(\"Check run\").first().json.defaultDatasetId + \"/items?format=json&clean=true&limit=5000\" }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 75000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": false,
      "notes": "Select your own Header Auth credential: Authorization = Bearer YOUR_APIFY_TOKEN. Do not paste a token into this export.",
      "notesInFlow": true
    },
    {
      "id": "results-and-summary",
      "name": "Results and summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "jsCode": "return [{json: {\n  runId: $('Check run').first().json.id,\n  summary: $('Run summary').first().json,\n  results: $input.all().map(item => item.json)\n}}];"
      }
    }
  ],
  "connections": {
    "Manual run": {
      "main": [
        [
          {
            "node": "Start run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start run": {
      "main": [
        [
          {
            "node": "Check run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check run": {
      "main": [
        [
          {
            "node": "Run summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run summary": {
      "main": [
        [
          {
            "node": "Check completeness",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check completeness": {
      "main": [
        [
          {
            "node": "Fetch variants",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch variants": {
      "main": [
        [
          {
            "node": "Results and summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}
```

See [n8n HTTP Request documentation](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/).

### Output and spending limits

`results` points to the default dataset. `summary` points to the SUMMARY record and reports processed inputs, unverified outcomes, truncated inputs, charged product count and budget stops. A successful process exit does not guarantee that every requested product was verified; inspect the statuses and summary.

The only paid event is `verified-product`, at $0.002. Platform usage is included in the event price for customers. The worker stops when the next product cannot fit within the run budget. No paid proxy, external AI API or separate server is used. We tested that four variants generate one product event, an unverified input generates none, and the worker stops before processing another product after its event budget is exhausted.

### Common questions

**Can I get Shopify variant IDs and SKUs?** Yes, from supported public product pages; omit `variantId` to request all returned variants.

**Can I detect a price mismatch?** Yes, for an exact variant with expected country and currency matching the observed market. The comparison is against your supplied value, not a historical price or an invented competitor price.

**Does it work on every Shopify store?** No. Unsupported, blocked or ambiguous records return UNVERIFIED rather than guessed values.

**Why can two USD prices differ?** Stores can publish different prices for different countries. Currency alone is not sufficient to verify the target market.

**Do I need a server?** No separate server is required to run this Actor on Apify. Python and n8n are optional ways to call it from your workflow.

# Actor input Schema

## `items` (type: `array`):

1–100 objects with url. Optional variantId (or ?variant=), expectedPrice, expectedCurrency, expectedPurchasable. Every expected-value check requires an exact variant ID and expectedCountry (uppercase two-letter code). This validates the country actually observed; it does not switch the store country. Use final public HTTPS /products/handle URLs. Blocked, unsupported or ambiguous results are unverified and have no verified-product fee.

## `maxResults` (type: `integer`):

Stop after this many extracted variant or diagnostic rows.

## Actor input object example

```json
{
  "items": [
    {
      "url": "https://www.andar.com/products/the-getaway"
    }
  ],
  "maxResults": 1000
}
```

# Actor output Schema

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

Verified variants and explicit unverified diagnostics, including observed market and source evidence.

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

Processed products, verification outcomes, charged product count and budget stop.

# 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 = {
    "items": [
        {
            "url": "https://www.andar.com/products/the-getaway"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("slatey_sea/my-actor").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 = { "items": [{ "url": "https://www.andar.com/products/the-getaway" }] }

# Run the Actor and wait for it to finish
run = client.actor("slatey_sea/my-actor").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 '{
  "items": [
    {
      "url": "https://www.andar.com/products/the-getaway"
    }
  ]
}' |
apify call slatey_sea/my-actor --silent --output-dataset

```

## MCP server setup

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

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/73ZJkvQzoJ7oxQh99/builds/7HDK9N3XUS86AksaI/openapi.json
