Shopify Variant Price & Availability Checker avatar

Shopify Variant Price & Availability Checker

Pricing

$2.00 / 1,000 verified products

Go to Apify Store
Shopify Variant Price & Availability Checker

Shopify Variant Price & Availability Checker

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.

Pricing

$2.00 / 1,000 verified products

Rating

0.0

(0)

Developer

İBRAHİM ÇAMYURDU

İBRAHİM ÇAMYURDU

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Categories

Security

Limited permissions

Share

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:

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

For an exact expected-price check, use:

{
"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.

CaseObserved storefront priceSupplied expectationOutcome
Andar, variant 42860017483858118.00 USD, US118.00 USD, USMATCH
Same Andar variant118.00 USD, USDeliberately changed to 118.01 USD, USMISMATCH
POPFLEX XXS, variant 4014398672085160.00 USD, US66.00 USD, TRUNVERIFIED

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:

[
{
"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

StatusMeaningCustom product event fee
EXTRACTEDRequested variants were extracted with supported currency and observed country$0.002 per complete input product
MATCHThe supplied expectations match in the verified country and currency$0.002 per complete input product
MISMATCHAt least one supplied expectation differs in that same verified market$0.002 per complete input product
UNVERIFIEDBlocked, unsupported, missing, conflicting or different-market dataNone

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.

"""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 and Get a run.

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.

{
"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.

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.