UK Grocery Price Matrix — Compare Supermarket Prices by EAN avatar

UK Grocery Price Matrix — Compare Supermarket Prices by EAN

Pricing

from $3.00 / 1,000 matrix rows

Go to Apify Store
UK Grocery Price Matrix — Compare Supermarket Prices by EAN

UK Grocery Price Matrix — Compare Supermarket Prices by EAN

Compare grocery prices across Tesco, Sainsbury's, Asda, Morrisons, Waitrose, Aldi, Ocado, Iceland, Co-op and Lidl UK in one run. Matches products by EAN barcode with a fuzzy-name fallback and flags the cheapest retailer per product.

Pricing

from $3.00 / 1,000 matrix rows

Rating

0.0

(0)

Developer

Studio Amba

Studio Amba

Maintained by Community

Actor stats

0

Bookmarked

37

Total users

4

Monthly active users

1.4 days

Issues response

a day ago

Last modified

Categories

Share

UK Grocery Price Matrix

A grocery price comparison across Tesco, Sainsbury's, Asda, Morrisons, Waitrose, Aldi, Ocado, Iceland, Co-op and Lidl UK in a single run. Give it a product name, and it returns one row per product per retailer: the price, any promotion, and whether that retailer carries the product at all.

This actor does not scrape a website itself. It runs our ten UK supermarket scrapers in parallel, matches what comes back by EAN barcode (with a fuzzy product-name fallback when a barcode is missing), and builds the comparison matrix from the results.

Why use this actor?

CPG pricing teams, category managers and price-comparison sites need to know one thing constantly: what does this SKU cost at every major UK grocer, right now. Pulling that by hand from ten retailer sites is slow and the barcode matching is the annoying part. This actor does the fan-out and the matching in one call and returns a flat table you can pivot in a spreadsheet or load into a pricing dashboard.

What it returns

Each row in the output is one product at one retailer. If a retailer doesn't stock the product, you still get a row for it, marked available: false, so you can tell "we checked and it's not there" apart from "we didn't check."

[
{
"query": "heinz baked beans",
"productName": "Heinz Baked Beans 415g",
"brand": "Heinz",
"ean": "5000157024671",
"retailer": "Tesco",
"price": 1.35,
"promoPrice": null,
"pricePerUnit": "£0.33/100g",
"currency": "GBP",
"available": true,
"url": "https://www.tesco.com/groceries/en-GB/products/...",
"imageUrl": "https://digitalcontent.api.tesco.com/...",
"matchType": "ean",
"cheapestRetailer": "Aldi",
"priceSpreadPct": 22.7,
"absenceReason": null
},
{
"query": "heinz baked beans",
"productName": "Heinz Baked Beans 415g",
"brand": "Heinz",
"ean": "5000157024671",
"retailer": "Aldi",
"price": 1.10,
"promoPrice": null,
"pricePerUnit": "£0.27/100g",
"currency": "GBP",
"available": true,
"url": "https://www.aldi.co.uk/heinz-baked-beans/p/...",
"imageUrl": "https://...",
"matchType": "ean",
"cheapestRetailer": "Aldi",
"priceSpreadPct": 22.7,
"absenceReason": null
},
{
"query": "heinz baked beans",
"productName": "Heinz Baked Beans 415g",
"brand": null,
"ean": "5000157024671",
"retailer": "Waitrose",
"price": null,
"promoPrice": null,
"pricePerUnit": null,
"currency": "GBP",
"available": false,
"url": null,
"imageUrl": null,
"matchType": "ean",
"cheapestRetailer": "Aldi",
"priceSpreadPct": 22.7,
"absenceReason": "not_found"
}
]

Use cases

  • CPG pricing managers tracking how their products are priced across the ten biggest UK grocers, spotting where a retailer is undercutting the recommended shelf price.
  • Price-comparison sites that need a live, EAN-matched feed instead of manually reconciling ten product catalogues.
  • Category managers building a competitor price index for a shopping basket of SKUs, refreshed on a schedule.
  • EAN/GTIN matching for any team that has a barcode list and wants to know which UK grocers stock each code and at what price.

How to scrape UK supermarket price data

  1. Enter one or more products in Search Queries, one per line. A product name (heinz baked beans) or an EAN barcode (5000157024671) both work.
  2. Choose which Retailers to include. Leave all ten selected for the full matrix, or narrow it down if you only care about a subset.
  3. Set Max Items Per Source to control how many candidate products each retailer contributes per query.
  4. Run the actor. All selected retailers are queried in parallel per query, so adding retailers doesn't multiply the run time.
  5. Open the dataset. Each matched product has one row per retailer: a price row where it's carried, an absence row where it isn't.
  6. Use cheapestRetailer and priceSpreadPct to sort straight to the products with the widest price gaps.

Pre-configured Tasks

Run these directly, no input needed:

Input

FieldTypeDescription
searchQueriesarrayProduct names or EAN barcodes to price-check. Defaults to ["heinz baked beans"].
retailersarrayWhich of the ten retailers to include (tesco, sainsburys, asda, morrisons, waitrose, aldi, ocado, iceland, coop, lidl). Empty means all.
maxItemsPerSourceintegerMax candidate products considered per retailer per query (1-50, default 15).
timeoutPerSourceSecsintegerMax seconds to wait for each retailer scraper before treating it as failed (30-600, default 150).
proxyConfigurationobjectProxy settings passed to the underlying retailer scrapers. UK residential is recommended.

Example input

{
"searchQueries": ["heinz baked beans", "cadbury dairy milk"],
"retailers": ["tesco", "sainsburys", "asda", "morrisons", "waitrose", "aldi", "ocado", "iceland", "coop", "lidl"],
"maxItemsPerSource": 15,
"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "GB" }
}

Output

FieldTypeDescription
querystringThe search query this row was matched against
productNamestringCanonical product name
groupIdstringSame value on every row of one product group in this run. Two groups can have the same productName (one product in two pack sizes), so group on this. It is numbered per run, so it changes between runs
stableGroupIdstringGroup id built from the product itself, the same in every run and every query: gtin-<barcode> from the first retailer in the group (in the order Tesco, Sainsbury's, Asda, Morrisons, Waitrose, Aldi, Ocado, Iceland, Co-op, Lidl) that publishes a barcode, otherwise <retailer>-<product id>. It changes only when the product it is built from leaves the group, for example when that retailer stops listing it
brandstringProduct brand, when exposed
eanstringNormalized EAN barcode used for matching, or null
retailerstringTesco, Sainsbury's, Asda, Morrisons, Waitrose, Aldi, Ocado, Iceland, Co-op or Lidl
pricenumberRegular price in GBP, null on an absence row
promoPricenumberCurrent promotional price, when on offer
pricePerUnitstringRetailer-formatted unit price
unitPriceValuenumberpricePerUnit as a number, converted to unitPriceUnit ("£0.60/100 G" gives 6.00 per kg)
unitPriceUnitstringkg, l or each
packSizestringPack size as the retailer states it, or as written in the product name when the retailer has no separate field
unitQuantitynumberTotal pack quantity in unitOfMeasure ("4 x 115g" gives 460)
unitOfMeasurestringg, ml or each
loyaltyPricenumberSingle-unit loyalty or member price (Tesco Clubcard, Sainsbury's Nectar, Morrisons More Card, Co-op Member, Ocado Smart Pass, Lidl Plus) when the retailer shows one. price and promoPrice stay the price without a loyalty card, and cheapestRetailer is worked out on those
loyaltySchemestringThe loyalty scheme behind loyaltyPrice or loyaltyPromotion
loyaltyPromotionstringLoyalty offer text as shown, including multi-buy deals that have no single-unit price
offerTextstringThe retailer's own offer text as shown ("2 for £4.00", "Now £2, Was £3.25", "Rollback"), when there is one
currencystringAlways GBP
availablebooleanFalse on an absence row. True means the retailer lists the product for this search; stock is in inStock
inStockbooleanThe retailer's own stock flag. null where the retailer publishes none (Waitrose, Aldi, Co-op)
retailerProductIdstringThe retailer's own product id (Aldi: SKU)
fetchedAtstringISO time the retailer data for this row was fetched
promoValidFromstringPromotion start date, where the retailer scraper publishes one, else null
promoValidUntilstringPromotion end date, where the retailer scraper publishes one, else null
urlstringProduct page URL at that retailer
imageUrlstringProduct image URL
matchTypestringHow this row joined the product group. ean: the same barcode as the group. name: the same brand and product found by name. equivalent: another retailer's own-label version of the same product type, size and variant (for example Tesco and Sainsbury's own semi skimmed milk 4 pints), so a like-for-like alternative but not the same item
retailerProductNamestringThe product name as this retailer lists it
retailerEanstringThis retailer's own barcode for the product, when it publishes one
cheapestRetailerstringRetailer with the lowest price for this product
priceSpreadPctnumberPercentage gap between the highest and lowest price found
absenceReasonstringnot_found (retailer checked, doesn't carry it) or source_failed (that retailer's scraper timed out or errored)

Matching logic

Products are matched across retailers by EAN/GTIN barcode first, with leading zeros stripped. A barcode match still has to agree on pack size when both retailers state it, because some retailers list a multipack under the single item's barcode. Without a shared barcode, the actor matches by name: pack size (weight, volume or count) must be known for both products and equal, variant words must agree (semi skimmed vs whole, medium vs large eggs, mature vs extra mature, flavours, organic, jumbo, UHT, seeded), and brands must be compatible. When a retailer sends no brand, the other product's brand must appear in its name. A national brand is never matched to an own-label product. Two own-label products from different retailers are matched only as equivalent. Every check is made against the first product of the group, so a group cannot drift from one product to another. matchType on every row says which kind of match it is.

Search coverage

All ten retailers are queried through keyword search (as of the September 2026 update; Asda via its Algolia index, Morrisons via its platform search endpoint, Waitrose via its public product sitemap with full detail-page fetches, Lidl scoped to its UK storefront). absenceReason: "not_in_sample" can still appear in rare fallback situations and deliberately does NOT claim the retailer lacks the product; a true not_found means the retailer's search returned no match.

FAQ

Does this actor scrape the retailer websites itself? No. It calls our own Tesco, Sainsbury's, Asda, Morrisons, Waitrose, Aldi, Ocado, Iceland, Co-op and Lidl UK scrapers and combines their output. Each of those actors is also available separately on the Store.

Why does Aldi show a price but I can't add it to a basket? Aldi UK doesn't run an online supermarket. Its catalogue is browse-only, so Aldi's price field reflects the current in-store price shown on aldi.co.uk, not something purchasable online. It's still a valid comparison price.

Why does one retailer show absenceReason: "source_failed" on every row? That retailer's scraper timed out or errored for that query. A single failing retailer never blocks the run; it just shows up as failed instead of not_found so you know the difference.

Can I search by EAN barcode instead of product name? No — none of the retailers' own search engines accept a GTIN/EAN (verified against each site, Aug-Sep 2026), so a bare barcode in searchQueries cannot return anything. Search by product name. EAN values DO appear in the output where retailers expose them (Tesco, Sainsbury's, Asda, Aldi, Ocado, Iceland, Co-op), and cross-retailer matching uses those barcodes — so a barcode list still works as a post-scrape join key on the results, just not as a search input.

How is priceSpreadPct calculated? (highest price - lowest price) / lowest price * 100, using the promo price where one is active, across whichever retailers actually carry the matched product. It's null when only one retailer has the product.

How many products can I check in one run? Add as many lines to searchQueries as you need. Each query launches its own round of retailer calls, so run time scales roughly linearly with the number of queries.

Do I need my own proxy or Bright Data key? No. Proxy and anti-bot handling is managed inside each underlying retailer scraper. Just run this actor with the default settings.

Limitations

  • Matching quality depends on EAN coverage. Retailers that don't expose a barcode (Morrisons, Aldi, and Waitrose unless barcode fetching is enabled on that actor) rely on fuzzy name matching, which can occasionally group similar-but-different pack sizes together.
  • Prices reflect each retailer's standard online delivery pricing at the time of the run and can vary by postcode or in-store.
  • Data is scraped from public retailer websites and may change without notice.

This matrix is built on our standalone UK grocery scrapers, each of which you can also run on its own for a full-catalogue pull:

We run this comparison as a managed service too: scheduled runs, a fixed SKU list, and delivery to your inbox, Google Sheets, or API, maintenance included. See studioamba.dev/services or email hello@studioamba.dev for a free data sample.

Pricing and cost

This actor charges a start fee (per GB of memory) and a small per-result fee — absence rows (not_found / source_failed) stay in the dataset and are not charged. Until 2026-10-17 every row is charged; from that date only rows with a price are. The ten retailer scrapers it launches each bill their own pricing to your account on top, exactly as if you ran them directly. A one-product run across all ten chains costs roughly $1.50-$2 at default settings; narrow retailers to what you need.

Known issues (Aug 2026)

  • A retailer that finds no match can be reported as source_failed instead of not_found; treat the two codes as "no data" rather than trusting the distinction until this note disappears.