Product Distribution Checker — EAN Coverage EU Sites avatar

Product Distribution Checker — EAN Coverage EU Sites

Pricing

from $10.00 / 1,000 sku-country checks

Go to Apify Store
Product Distribution Checker — EAN Coverage EU Sites

Product Distribution Checker — EAN Coverage EU Sites

Give a brand's EAN/product list and target countries. Runs our Geizhals and Skroutz price-comparison scrapers (Ceneo, Prisjakt, Idealo, Heureka optional), returns every matched listing per EAN per country, plus an explicit absence row when nothing matched.

Pricing

from $10.00 / 1,000 sku-country checks

Rating

0.0

(0)

Developer

Studio Amba

Studio Amba

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Share

Product Distribution Checker — EAN Coverage Across EU Price Sites

Give this actor a brand's EAN/product list and the countries you care about. It runs our own European price-comparison scrapers in parallel and tells you, per EAN per country: is this product listed, where, and at what price — and just as importantly, where it is confirmed NOT listed.

Channel managers, distributors, and brand protection teams pay agencies for exactly this check. This actor runs it on demand or on a schedule instead.

What it does

  • Runs our own price-comparison scrapers. Geizhals (AT/DE) and Skroutz (GR) by default — the two engines cloud-verified working end-to-end on a plain residential proxy. Ceneo (PL), Prisjakt (SE), Idealo (DE) and Heureka (CZ) are all available opt-in; see the reliability table below before adding them.
  • Matches each engine's result back to your product. EAN/barcode match when an engine exposes one (none currently do — see Honest Limitations), fuzzy product-name match otherwise.
  • Returns every matching listing, not just the cheapest one — if an engine's search turns up three variants of your product (different colors, bundle sizes), you get all three rows.
  • Emits an explicit absence row when a country was actually checked and nothing matched. present: false is not a missing row, it's a returned row — the proof that the engine ran, searched, and found nothing for your EAN in that market.
  • Never fakes an absence from a failed check. If the underlying engine's own run failed or timed out, no row is emitted for that product/country/engine at all — a real infrastructure failure and a confirmed "not listed" are different facts, and this actor keeps them separate rather than silently treating a broken check as a distribution gap.

How to scrape product distribution data across Europe

  1. Enter your products — each one needs an ean (barcode) and a productName (used as the search query, since none of the bundled engines support searching by EAN directly). brand is optional and carried through to the output.
  2. Pick your countriesAT, DE, GR are covered by the cloud-verified healthy default engines. Add PL, SE, CZ and opt in the matching engine via engines if you need attempted coverage there.
  3. Run it. Each row is one product/country/engine check: a matched listing (present: true, with price and URL) or a confirmed gap (present: false).
  4. Group the output by ean + country downstream to build a coverage matrix — which markets carry the SKU, which don't, and at what price where it does.
  5. Schedule it to catch new listings or new gaps as resellers add or drop a SKU.

Input

FieldTypeDescription
productsarray{ ean, productName, brand? } objects to check. Defaults to a demo Sony product.
countriesarrayISO country codes to check. Default ["AT", "DE", "GR"] (the cloud-verified healthy default).
enginesarrayOpt-in engines beyond the default: ceneo, prisjakt, idealo, heureka. See reliability table.
matchThresholdnumberMinimum fuzzy name-similarity (0-1) to count as a match. Default 0.35.
maxItemsPerSourceintegerSearch results pulled per engine per product before matching. Default 8.
timeoutPerSourceSecsintegerPer-engine call timeout before treating that check as failed. Default 150.
proxyConfigurationobjectOverrides the built-in per-engine RESIDENTIAL default.

Output fields

FieldTypeDescription
eanstringProduct EAN/barcode from your input
productNamestringProduct name from your input
brandstring | nullBrand from your input, if provided
countrystringISO country code this row checks
enginestringgeizhals, skroutz, ceneo, prisjakt, idealo, or heureka
sellerNamestringThe comparison site itself (see Honest Limitations — not a named reseller)
pricenumber | nullListed price in the local currency; null on an absence row
currencystring | nullLocal market currency, populated even on an absence row
urlstringLink to the matched listing; empty string on an absence row
presentbooleantrue = matched listing found. false = engine checked, nothing matched
matchTypestring | nullean, name, or null on an absence row
matchScorenumber | null0-1 fuzzy match confidence; 1 for an EAN match; null on absence
offerCountnumber | nullResellers the engine aggregated this price from, where exposed
scrapedAtstringISO timestamp of the run

Example output

Present row:

{
"ean": "4548736112212",
"productName": "Sony WH-1000XM5",
"brand": "Sony",
"country": "AT",
"engine": "geizhals",
"sellerName": "Geizhals",
"price": 189,
"currency": "EUR",
"url": "https://geizhals.at/sony-wh-1000xm5-v101824.html",
"present": true,
"matchType": "name",
"matchScore": 1,
"offerCount": 80,
"scrapedAt": "2026-07-20T08:35:35.179Z"
}

Confirmed-absence row (cloud-verified — a genuinely nonexistent SKU checked against Geizhals AT/DE):

{
"ean": "9999999999999",
"productName": "Zorvexnaught Widget XQ7000-Nonexistent",
"brand": "Zorvexnaught",
"country": "AT",
"engine": "geizhals",
"sellerName": "Geizhals",
"price": null,
"currency": "EUR",
"url": "",
"present": false,
"matchType": null,
"matchScore": null,
"offerCount": null,
"scrapedAt": "2026-07-20T08:39:42.770Z"
}

Honest limitation: no per-seller data

None of the bundled engines expose a per-seller/per-merchant breakdown — each returns a single aggregate "lowest price found" per matched product, plus (on some) a count of how many resellers that price was drawn from. There is no seller name, no per-offer list.

So sellerName is honestly the comparison engine itself (e.g. "Geizhals"), not a named reseller. offerCount tells you how many resellers that price was pulled from, where the engine exposes it. Reseller-level attribution would require scrapers built against individual retailer sites, not price-comparison aggregators — a different, much larger build.

Honest limitation: EAN matching is architecturally ready, not yet live

Matching checks an exact EAN first, then falls back to fuzzy name matching. As of this actor's cloud verification (2026-07-20), none of the 6 bundled engines return an EAN/barcode field in their own output, so every match today is a name match — never hidden, always visible in matchType. If a child scraper adds EAN extraction later, this actor picks it up automatically with no code change on this end.

Engines and reliability (method disclosure)

Cloud-verified 2026-07-20 — every engine below was actually run with a real query ("iphone 15") right before this actor shipped, not just read from its source code.

EngineMarketCurrencyMethodReliabilityNotes
GeizhalsAT/DEEURResidential proxy + parsingHighCloud-verified: real products, real prices, real matches. Default.
SkroutzGREURResidential proxy + parsingHighCloud-verified: real products, real prices (4 iPhone 15 variants returned in the live test). Default.
IdealoDEEURBright Data Scraping BrowserBrokenLive test hit "A global adaptive rate limit has been applied" then an anti-bot block — 0 products. Opt-in.
CeneoPLPLNResidential proxy + parsingBrokenLive test TIMED OUT. Opt-in.
HeurekaCZCZKResidential proxy + parsingBrokenLive test returned only nav/category chrome ("Speciální nabídky", "Elektronika", "Letní akce") — a parser bug on the child, not real product data. Opt-in.
PrisjaktSESEKResidential proxy + parsingBlockedLive test returned 0 items — Cloudflare "Just a moment..." challenge even through a residential Swedish IP. Opt-in.

All engines get a RESIDENTIAL proxy scoped to their market by default. The actor degrades gracefully: if an engine's run fails, the run continues with the others and no row is emitted for that failed check (see "failed vs. absent" below) — it never gets silently counted as a confirmed gap.

Failed check vs. confirmed absence — why this matters

This is the core design decision in this actor, and it's worth being explicit about:

  • Confirmed absence (present: false, row emitted): the child engine's run SUCCEEDED and returned data, but nothing in it matched your EAN/product. The check happened; the answer is "not found here."
  • Failed check (no row emitted at all): the child engine's run errored, timed out, or hit its own zero-results guard and failed itself. We do not know whether the product is listed or not — we just couldn't check. Emitting a present: false row here would be a false negative dressed up as data.

Cloud-verified case (2026-07-20): checking a nonexistent product against Geizhals (AT/DE) and Skroutz (GR) — Geizhals's run succeeded, returned 5 unrelated products, none matched, so 2 confirmed-absence rows were pushed (AT, DE). Skroutz's own run failed with its own zero-results guard ("Zero results scraped... Check your search query"), so zero rows were pushed for GR — not a false absence claim. Check the run summary's checksFailed count if you need to know which product/engine pairs couldn't be checked.

A run with zero present rows because nothing matched is a success, not a failure — see the run summary. The actor only calls Actor.fail() when every single engine call across the whole run failed, meaning nothing could be checked at all.

Documented limits

  • Query is by product name, not EAN. None of the bundled engines support searching by barcode directly.
  • Fuzzy name matching can miss or mismatch. A generic product name risks matching the wrong SKU or matching too many variants. Use a specific model name and tune matchThreshold as needed.
  • Country coverage is limited to what the default 2 engines serve (AT, DE, GR) unless you opt in the broken/blocked engines and accept a real chance of failed checks until they're healed.
  • This is presence/absence on comparison ENGINES, not exhaustive retail coverage. A product genuinely sold direct-to-consumer only, or through retailers this actor's 6 engines don't aggregate, will show as absent even if it's sold somewhere.

Who uses this

  • Brand channel managers verifying which markets and resellers actually carry a SKU before a launch or a distribution audit.
  • Distributors proving coverage gaps to a brand, or proving a claimed distributor relationship is actually live in-market.
  • Pricing teams building a coverage-by-country matrix without manually searching six sites per SKU.

Pricing

Pay per run start and per row returned — present or absent, each row is one sku-country check, and a confirmed gap is exactly as much of a deliverable as a confirmed listing. See the pricing tab for current rates.

FAQ

Does an absence row mean the product definitely isn't sold in that country? It means the engines this actor checks didn't have a matching listing at the time of the run. See "Documented limits" — this is engine-level coverage, not exhaustive retail coverage.

Why isn't every country in my request necessarily covered? Only Geizhals (AT/DE) and Skroutz (GR) are cloud-verified healthy by default. If you request PL/SE/CZ without opting in the matching engine, the run summary lists them as countriesUnsupported rather than silently returning nothing or faking an absence.

Do I need a proxy? No — every engine defaults to a RESIDENTIAL proxy scoped to its market. Supply your own proxyConfiguration only if you want to override that.

Is this legal? The actor collects publicly advertised listings from public comparison sites. You are responsible for how you use the output.