Product Distribution Checker — EAN Coverage EU Sites
Pricing
from $10.00 / 1,000 sku-country checks
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
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: falseis 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
- Enter your products — each one needs an
ean(barcode) and aproductName(used as the search query, since none of the bundled engines support searching by EAN directly).brandis optional and carried through to the output. - Pick your countries —
AT,DE,GRare covered by the cloud-verified healthy default engines. AddPL,SE,CZand opt in the matching engine viaenginesif you need attempted coverage there. - 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). - Group the output by
ean+countrydownstream to build a coverage matrix — which markets carry the SKU, which don't, and at what price where it does. - Schedule it to catch new listings or new gaps as resellers add or drop a SKU.
Input
| Field | Type | Description |
|---|---|---|
products | array | { ean, productName, brand? } objects to check. Defaults to a demo Sony product. |
countries | array | ISO country codes to check. Default ["AT", "DE", "GR"] (the cloud-verified healthy default). |
engines | array | Opt-in engines beyond the default: ceneo, prisjakt, idealo, heureka. See reliability table. |
matchThreshold | number | Minimum fuzzy name-similarity (0-1) to count as a match. Default 0.35. |
maxItemsPerSource | integer | Search results pulled per engine per product before matching. Default 8. |
timeoutPerSourceSecs | integer | Per-engine call timeout before treating that check as failed. Default 150. |
proxyConfiguration | object | Overrides the built-in per-engine RESIDENTIAL default. |
Output fields
| Field | Type | Description |
|---|---|---|
ean | string | Product EAN/barcode from your input |
productName | string | Product name from your input |
brand | string | null | Brand from your input, if provided |
country | string | ISO country code this row checks |
engine | string | geizhals, skroutz, ceneo, prisjakt, idealo, or heureka |
sellerName | string | The comparison site itself (see Honest Limitations — not a named reseller) |
price | number | null | Listed price in the local currency; null on an absence row |
currency | string | null | Local market currency, populated even on an absence row |
url | string | Link to the matched listing; empty string on an absence row |
present | boolean | true = matched listing found. false = engine checked, nothing matched |
matchType | string | null | ean, name, or null on an absence row |
matchScore | number | null | 0-1 fuzzy match confidence; 1 for an EAN match; null on absence |
offerCount | number | null | Resellers the engine aggregated this price from, where exposed |
scrapedAt | string | ISO 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.
| Engine | Market | Currency | Method | Reliability | Notes |
|---|---|---|---|---|---|
| Geizhals | AT/DE | EUR | Residential proxy + parsing | High | Cloud-verified: real products, real prices, real matches. Default. |
| Skroutz | GR | EUR | Residential proxy + parsing | High | Cloud-verified: real products, real prices (4 iPhone 15 variants returned in the live test). Default. |
| Idealo | DE | EUR | Bright Data Scraping Browser | Broken | Live test hit "A global adaptive rate limit has been applied" then an anti-bot block — 0 products. Opt-in. |
| Ceneo | PL | PLN | Residential proxy + parsing | Broken | Live test TIMED OUT. Opt-in. |
| Heureka | CZ | CZK | Residential proxy + parsing | Broken | Live 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. |
| Prisjakt | SE | SEK | Residential proxy + parsing | Blocked | Live 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: falserow 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
matchThresholdas 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.