--- name: ecommerce-platform-detector description: Identify which e-commerce platform a storefront runs on and pull its catalogue, via the Apify Actor arman-bd/ecommerce-platform-detector. Returns the detected platform (Shopify, WooCommerce, BigCommerce, Squarespace, Magento or unknown), a 0-100 confidence score, the evidence behind it, and one normalised product row per item with title, price, compare-at price, currency, SKU, availability, variant count, images and product URL. Use for tech-stack qualification of a lead list, cross-platform competitor catalogue audits, or a price monitor that must work regardless of what each store is built on. Not for variant-level rows, reviews, stock quantities, order or customer data, or stores behind a login. --- # E-commerce Platform Detector Apify Actor `arman-bd/ecommerce-platform-detector`. Give it storefront URLs. It works out which of five platforms each one runs on, then reads the catalogue through whatever public route that platform offers, and emits one product row per item in a single shape regardless of the platform underneath. Detection evidence rides on every record, so the classification can be audited rather than trusted. ## When to use it - Qualify a list of domains by tech stack before a sales or partnership push. - Audit a competitor set whose members sit on different platforms, with one output schema. - Build a price monitor across a mixed portfolio and diff on `productId` plus `price`. - Detect re-platforming over time by scheduling the run and watching `detectedPlatform`. - Get a catalogue out of a store you have no integration with, without writing a per-platform adapter. ## When not to use it - Variant-level pricing or per-size stock. One row per product; `price` is the lowest live variant price and `variantCount` says how many there are. - Reviews, ratings, inventory counts, orders, customers or anything behind a login. - Marketplaces and platforms outside the five fingerprinted here. They come back as `unknown`, and the catalogue is then only as good as the store's own structured markup. - A single product page lookup. This Actor works on whole stores. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/ecommerce-platform-detector').call({ storeUrls: ['https://www.allbirds.com', 'barefootbuttons.com'], detectOnly: false, maxProductsPerStore: 250, platforms: [], }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~ecommerce-platform-detector/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"storeUrls":["allbirds.com","gymshark.com"],"detectOnly":true}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/ecommerce-platform-detector`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `storeUrls` | string[] | yes | | Store home pages. A bare domain such as `allbirds.com` is fine; the scheme is added, redirects are followed, and if the host you gave fails outright the www or apex variant is tried once. Pass a locale sub-path such as `ikea.com/us/en/` for a multi-region store and that region's URLs are preferred. Duplicates are removed. | | `detectOnly` | boolean | no | `false` | Emit one detection record per store and skip catalogue extraction entirely. Roughly two to four requests per domain. | | `maxProductsPerStore` | integer | no | `100` | Cap per store, not per run. `0` means no cap, which on a large Shopify or Magento store means tens of thousands of rows, so raise the run timeout to match. | | `platforms` | string[] | no | `[]` | Restrict *extraction* to stores detected as one of `shopify`, `woocommerce`, `bigcommerce`, `squarespace`, `magento` or `unknown`. Every store is still detected and still appears in `RUN_SUMMARY.stores`; non-matching ones are skipped before extraction. An unrecognised value throws before the run starts. Empty means process everything. | **`detectOnly` versus a `platforms` filter is the decision that matters.** `detectOnly: true` is the cheap census: one record per domain, no catalogue, fine over hundreds of URLs. A `platforms` filter is the middle ground: you still get the full tech-stack breakdown in `RUN_SUMMARY.stores` for every domain, but you only pay to extract catalogues from the platforms you named. Full extraction over a long mixed list is the expensive shape, and `maxProductsPerStore` is the only thing standing between you and a store with 40,000 products. ## Output In extraction mode, one record per product, with the store's detection fields repeated on every row. In `detectOnly` mode, one record per store instead, carrying the detection fields plus three extras and no product fields at all. | Field | Type | Notes | |---|---|---| | `store` | string | Hostname after redirects. Not always what you passed in: a bare domain that redirects to www comes back as the www host. | | `storeUrl` | string | Canonical origin after redirects. | | `detectedPlatform` | string | `shopify`, `woocommerce`, `bigcommerce`, `squarespace`, `magento` or `unknown`. | | `confidence` | number | 0 to 100. At 30 or above a platform is named; below that the store is reported as `unknown` and `confidence` is capped at 29. | | `detectionSignals` | string[] | The individual markers that produced the score, each prefixed `header:`, `cookie:`, `html:` or `probe:`. For a named platform only that platform's markers are listed; for `unknown`, everything observed is carried through. | | `extractionMethod` | string | `shopify-products-json`, `woocommerce-store-api`, `squarespace-format-json`, `magento-graphql` or `jsonld-sitemap`. The route the products actually came from, which is not always the one the platform implies. | | `productId` | string \| number \| null | Platform-native identifier. A number on Shopify and WooCommerce, a string elsewhere. Do not assume a type. | | `title` | string \| null | Product name. | | `handle` | string \| null | URL slug where the platform exposes one. | | `vendor` | string \| null | Brand. Often null outside Shopify. | | `productType` | string \| null | Platform-native category. Often null. | | `price` | number \| null | Lowest current variant price, already in major units. | | `compareAtPrice` | number \| null | The struck-through original where the platform reports one, otherwise null. | | `currency` | string \| null | ISO code. Null when the store publishes no machine-readable currency. | | `available` | boolean \| null | Whether any variant is purchasable. Null when the route cannot tell. | | `sku` | string \| null | | | `variantCount` | number \| null | How many variants sit behind the single row. | | `images` | string[] | Up to 10 image URLs, truncated at 10 rather than paginated. | | `url` | string \| null | Direct link to the product page. | | `platformScores` | object | **`detectOnly` only.** Every platform that scored, not just the winner: `{ magento: 70 }`. | | `httpStatus` | number | **`detectOnly` only.** Status of the home-page fetch. | | `finalUrl` | string | **`detectOnly` only.** Where the home page landed after redirects, including any locale path. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A product record, image list trimmed: ```json { "store": "www.allbirds.com", "storeUrl": "https://www.allbirds.com", "detectedPlatform": "shopify", "confidence": 100, "detectionSignals": [ "header: powered-by: Shopify response header", "cookie: _shopify_* session cookie", "html: cdn.shopify.com asset host" ], "extractionMethod": "shopify-products-json", "productId": 7292464955472, "title": "Men's Cruiser - Shadow Blue (Natural White Sole)", "handle": "mens-cruiser-shadow-blue-natural-white-sole", "vendor": "Allbirds", "productType": "Shoes", "price": 105, "compareAtPrice": null, "currency": "USD", "available": true, "sku": "A12856M080", "variantCount": 13, "images": ["https://cdn.shopify.com/s/files/1/1104/4168/files/All-birds_0010.png?v=1783535519"], "url": "https://www.allbirds.com/products/mens-cruiser-shadow-blue-natural-white-sole", "scrapedAt": "2026-08-06T11:49:23.335Z" } ``` A `detectOnly` record for a headless storefront, identified with no markup markers at all: ```json { "store": "www.bulk.com", "storeUrl": "https://www.bulk.com", "detectedPlatform": "magento", "confidence": 70, "detectionSignals": ["probe: …", "probe: …"], "platformScores": { "magento": 70 }, "extractionMethod": "magento-graphql", "httpStatus": 200, "finalUrl": "https://www.bulk.com/uk/", "scrapedAt": "2026-08-06T11:52:04.118Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** With one row per product rather than per store, this is the only place the per-store picture exists. ```json { "storesRequested": 5, "storesFailed": 0, "failures": [ { "store": "shop.example.com", "stage": "extract", "platform": "bigcommerce", "error": "no products found via jsonld-sitemap (scanned 12 sitemap URL(s))" } ], "stores": [ { "store": "www.allbirds.com", "storeUrl": "https://www.allbirds.com", "detectedPlatform": "shopify", "confidence": 100, "detectionSignals": ["…"], "extractionMethod": "shopify-products-json", "products": 291 }, { "store": "barefootbuttons.com", "storeUrl": "https://barefootbuttons.com", "detectedPlatform": "woocommerce", "confidence": 100, "detectionSignals": ["…"], "extractionMethod": "woocommerce-store-api", "products": 61 }, { "store": "www.aaksonline.com", "storeUrl": "https://www.aaksonline.com", "detectedPlatform": "squarespace", "confidence": 100, "detectionSignals": ["…"], "skipped": "platform filter", "products": 0 } ], "platformBreakdown": { "shopify": 1, "woocommerce": 1, "squarespace": 1 }, "itemsSaved": 412, "filters": { "detectOnly": false, "maxProductsPerStore": 0, "platforms": [] }, "finishedAt": "2026-08-06T11:53:41.902Z" } ``` `stores` holds one entry per store that got as far as detection, and it is the object to read. `products` is the row count that store contributed. `skipped` is present with the value `platform filter` when `platforms` excluded it. `truncated` is present, holding a short explanation, when the catalogue was cut short by a pagination ceiling or a mid-catalogue failure: **a run can be complete and still truncated per store**, so check for that key before treating a catalogue as exhaustive. `detectOnly` is present and true on entries from a detection-only run. `storesFailed` counts detection failures only; extraction failures live in `failures` with `stage: "extract"` and are not counted there. ## Behaviour to plan around - **Detection fields are repeated on every product row.** They are store-level facts, so group by `store` before counting platforms, or read `platformBreakdown` instead. - **`unknown` is a result, not an error.** A store below the confidence threshold is still put through the structured-markup route and often returns a full catalogue. Only a store that could not be reached at all counts as a failure. - **`extractionMethod` can disagree with `detectedPlatform`.** Extraction degrades rather than failing: a store that is confidently Shopify but has its catalogue route switched off falls back to `jsonld-sitemap` and still returns products. BigCommerce publishes no anonymous catalogue route at all, so it always takes `jsonld-sitemap`. - **`price` is a lowest-variant price.** Comparing it between runs detects a sale starting on any variant, not a price change on a specific one. - **Empty catalogues are logged as failures without stopping anything.** A store that detected fine but yielded zero products lands in `failures` with `stage: "extract"`, and its `stores` entry still exists with `products: 0`. - **Pagination has ceilings.** Shopify stops at page 40, WooCommerce at page 120, Magento at page 50, and the structured-markup route scans at most 12 sitemap files plus 6 children of an index. Any of these sets `truncated` on the store's summary entry. - **`maxProductsPerStore` is per store.** A run over 20 stores with the default returns up to 2000 rows, not 100. - **A locale matters.** `ikea.com` and `ikea.com/us/en/` produce different currencies and different product sets. Pass the sub-path when you care which. - **Transient failures are retried** up to three times with linear backoff. A 4xx from a detection probe is treated as evidence that the platform is absent rather than as an error, so it never triggers a retry. - **One store failing never aborts the run.** The Actor only throws when no store could be processed at all. ## Recipes **Qualify a lead list by tech stack.** The cheap mode: no catalogue, one row per domain. ```json { "storeUrls": ["allbirds.com", "gymshark.com", "bulk.com", "aaksonline.com"], "detectOnly": true } ``` Read `detectedPlatform` and `confidence` per row, and `platformScores` when you want to see whether a store scored at all before being called `unknown`. **Competitor catalogue audit across mixed platforms.** One schema out, whatever each store runs on. ```json { "storeUrls": [ "https://www.allbirds.com", "https://barefootbuttons.com", "https://www.berlinpackaging.com", "https://www.aaksonline.com" ], "maxProductsPerStore": 250 } ``` Group rows by `store`, then compare `price` distributions and `variantCount`. **Price monitor on the platforms with exact prices.** Everything is still detected; only the named platforms are extracted. ```json { "storeUrls": ["https://www.allbirds.com", "https://barefootbuttons.com"], "platforms": ["shopify", "woocommerce"], "maxProductsPerStore": 1000 } ``` Schedule it and diff on `productId` plus `price`, dropping any store whose summary entry carries `truncated`. **Full catalogue of one store.** No cap, one target, long timeout. ```json { "storeUrls": ["https://www.allbirds.com"], "maxProductsPerStore": 0 } ``` Check `RUN_SUMMARY.stores[0].products` against the store's own product count before treating the export as complete.