Shopify Variant Price & Availability Checker
Pricing
$2.00 / 1,000 verified products
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Security
Limited permissionsShare
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.
| 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:
[{"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/handleURL; an optional locale prefix is supported.variantId: an exact numeric variant ID, or use?variant=IDin the URL. Omit it to request all returned variants.expectedPrice: a nonnegative decimal string with at most two decimal places; requiresexpectedCurrency.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.
python check_prices.pyprints an offline preview and makes no API call.- Set
APIFY_TOKENin your local environment. Never put a token in a URL or shared file. python check_prices.py --run --max-cost-usd 0.01starts one run and writesprice-results.json.- 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 argparseimport jsonimport osimport reimport timefrom decimal import Decimalfrom pathlib import Pathfrom urllib.parse import urlencodefrom urllib.request import Request, urlopenACTOR_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 valuedef 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() + 240while 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') == 0and 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 INPUTif not args.run and not args.resume:print(json.dumps({'mode': 'OFFLINE_PREVIEW', 'maxCostUsd': str(amount), 'input': payload}, indent=2))returntoken = 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.
- Create an HTTP Header Auth credential: header name
Authorization, valueBearer YOUR_APIFY_TOKEN. - Select that credential in Start run, Run summary and Fetch variants. This export contains no token or credential ID.
- 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. - 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.