UK Grocery Prices: Tesco, Sainsbury's, Asda, Aldi avatar

UK Grocery Prices: Tesco, Sainsbury's, Asda, Aldi

Pricing

from $1.50 / 1,000 products

Go to Apify Store
UK Grocery Prices: Tesco, Sainsbury's, Asda, Aldi

UK Grocery Prices: Tesco, Sainsbury's, Asda, Aldi

Shelf prices as published online for Tesco, Sainsbury's, Asda and Aldi in one schema, with EAN barcodes where published, single-item Tesco Clubcard and Sainsbury's Nectar prices where shown, and promotions. Independent; not affiliated with the retailers named.

Pricing

from $1.50 / 1,000 products

Rating

0.0

(0)

Developer

Russell Yapp

Russell Yapp

Maintained by Community

Actor stats

0

Bookmarked

53

Total users

14

Monthly active users

2 days ago

Last modified

Categories

Security

Limited permissions

Share

UK Supermarket Price Scraper: Tesco, Sainsbury's, Asda and Aldi price data

Each row is one product at one shop: the shelf price, was-price, loyalty price (Clubcard, Nectar or More Card), unit price, promotion text, stock, EAN barcode and the time it was read (scrapedAt), in one schema for Tesco, Sainsbury's, Asda, Aldi and Morrisons (opt-in). Where a shop publishes no barcode or stock, the field is empty (ean: [], inStock: null).

Independent tool. Not affiliated with, endorsed by or sponsored by Tesco, Sainsbury's, Asda, Aldi or Morrisons. Their names and brands are trademarks of their owners and are named only to say where the data comes from.

Cost (Free plan, batch runs): $0.005 per run plus $0.0025 per product returned; higher Apify plans pay less. A run that returns nothing costs only the start fee. The basket endpoint is priced per matched line (see the Pricing section).

Worked example. Searching semi skimmed milk at Tesco and Aldi with maxItems set to 20 returns at most 20 products and costs at most 20 × $0.0025 + $0.005 = $0.055 on the Free plan.

What $5 of Apify credit buys. If your plan includes $5 of monthly credit, at the prices above and for batch runs:

  • about 19 default runs (search milk at Aldi and Sainsbury's, up to 100 products, at most $0.255 each);
  • about 6 runs of 300 products ($0.755 each);
  • about 1,998 products in a single run (($5 − $0.005) ÷ $0.0025);
  • not a Morrisons catalogue-mode run, which returned about 26,000 rows (the 29 September 2026 check run) and costs about $66 on the Free plan at that size.

Modes and the questions they answer

ModeUse it to answerInput
search (default)"What does semi-skimmed milk cost at Tesco and Aldi today?" (set maxItems to 20 for a quick check)queries, retailers
category"List the products in this Tesco aisle with their prices (up to maxItems rows)."categoryUrls
product"What is the price today of this exact product page?"productUrls
catalogue"Read Morrisons' category tree and product pages in one run" (Morrisons only; about 26,000 rows in 3 to 4 hours in the 29 September 2026 check run, about $66 on the Free plan, see the Catalogue mode (Morrisons) section)retailers: ["morrisons"], maxItems: 0
POST /basket (Standby)"What does my shopping list cost at each shop, and which shop is cheapest?" See the Price a basket in one call sectiona basket in the request body

Three questions to start with:

  1. "What does semi-skimmed milk cost at Tesco and Aldi today?" (search: queries: ["semi skimmed milk"], retailers: ["tesco", "aldi"])
  2. "Which products in this Asda category are on Rollback?" (category: read wasPrice and promotionText)
  3. "Which shop is cheapest for this shopping list?" (the basket endpoint)

When to use it, and when not to

Use it for current shelf prices, was-prices, loyalty prices, promotions and barcodes of named products, of categories or of a shopping list, across the UK supermarkets above.

It does not do these, and no input changes that:

  • Accounts and orders. No login, no basket or order data, no delivery slots, no delivery or collection prices; the Actor is designed not to include individuals' details.
  • Store-level prices where the retailer shows none. No store is chosen for Tesco and Sainsbury's; one fixed store (Asda, set by asdaStoreId) or service point (Aldi) is used for the others, and prices in other stores, formats and regions may differ. Asda stock (inStock) is for that one store; Aldi reports no stock and no barcode.
  • Morrisons by default. Morrisons is opt-in: add it to retailers to include it.
  • History. A run returns that day's prices and does not compare them with earlier runs; see the Free daily dataset section and the recipes below for history.

Each run returns that day's prices and does not compare them with earlier runs. To build a history, use the free daily dataset below or append rows to a dataset you name with outputDataset.

{"retailer":"sainsburys","retailerProductId":"357937","ean":["0000000357937","0000050181117"],"name":"Sainsbury's British Semi Skimmed Milk 2.27L (4 pint)","brand":"Sainsbury's","packSize":"2.27L","price":1.65,"wasPrice":null,"unitPrice":{"value":0.73,"measure":"l"},"loyaltyPrice":null,"promotionText":null,"promotionEndsAt":null,"inStock":true,"categoryPath":["Hot drinks, soft drinks & water","Milk & milk drinks","Fresh milk"],"url":"https://www.sainsburys.co.uk/groceries/product/sainsburys-british-semi-skimmed-milk-2-27l-4-pint","imageUrl":"https://example.com/images/product.jpg","rating":{"value":4.61,"count":2135},"scrapedAt":"2026-10-09T05:30:10Z","matchKey":"sainsburys british semi skimmed milk 2.27l 4 pint 4pt","matchKeyV1":"sainsburys british semi skimmed milk 2.27l 4 pint 2270ml","matchBasis":"brand-name-size","matchConfidence":0.97}

In this sample matchConfidence is the fixed value for the brand-name-size basis, not a score for this product.

A default run (search "milk" in Aldi and Sainsbury's, up to 100 products) costs at most $0.255 on the Free plan: $0.005 to start and $0.0025 per product.

What you get: the UK supermarket prices dataset

RetailerModesBarcode (EAN) coverageLoyalty / promotion fieldsStock
Tescosearch, category, productOn nearly every row (99.1% in the 4 October 2026 snapshot): the data endpoint Tesco's website uses returns a GTIN for nearly all productsloyaltyPrice carries single-item Clubcard prices, e.g. £1.60 Clubcard Price; multibuy Clubcard offers and meal deals go in promotionText only ( wasPrice is always null)Tesco's own online-availability flag (no store is chosen for Tesco)
Sainsbury'ssearch, category, productNearly every row — EANs come from Sainsbury's own product data (codes that fail the check digit are dropped)loyaltyPrice carries the Nectar price ( price stays the regular shelf price); promotionText for other offers, e.g. 2 for £3Sainsbury's own availability flag
Asdasearch, category, productAbout 99% of rows (98.9% in the 4 October 2026 snapshot) — recovered from Asda's product image id and validated as a GTIN; the rest have nonewasPrice carries Rollback was-prices; promotionText carries multibuys, e.g. Any 2 for £4Per store, set by the asdaStoreId input (default 4565); we have not checked whether Asda prices change with the store
Aldisearch, category, productNone — Aldi publishes no barcode anywhere, so rows carry an empty ean (see the Joining retailers section)wasPrice on in-store markdowns; no loyalty priceNot published — inStock is always null
Morrisons (opt-in)search, category, product, catalogueNone — Morrisons publishes no barcode, so rows carry an empty ean (see the Joining retailers section)loyaltyPrice carries the More Card price; wasPrice and promotionText carry other reductions and offersMorrisons' own availability flag

Every shop in one run, without a small per-shop cap. A run returns up to maxItems rows in total, 100 by default and split evenly between the shops you choose; set maxItems to 0 for no cap. Tesco, Sainsbury's, Asda, Aldi and Morrisons come back in one schema, and the run summary gives each shop's row count and status, so a shop that found nothing is reported rather than silently missing. For price history without a run, the free daily dataset prices the same 212 staples at four shops every morning since 24 September 2026. You pay per product row returned, at your plan's price in Pricing, plus the run start: nothing per search, page or shop.

Joining retailers: ean, matchKey and matchConfidence

Join on ean first: two rows that share an EAN are the same product. Then join on matchKey, a normalised brand + name + pack size, and read each row's matchConfidence. In the 4 October 2026 weekly snapshot, 97.6% of the cross-retailer pairs with the same matchKey that could be checked against a barcode were the same product (4,124 of 4,225). matchKey only joins products named the same way, so it finds only some of the pairs a barcode would: retailers word the same product differently. Aldi publishes no barcode and sells mostly its own labels, and in that snapshot no Aldi matchKey equalled another shop's; Morrisons publishes no barcode either. To compare Aldi or Morrisons with the others, compare by unitPrice, or use the basket endpoint, which matches by name with a confidence score. Two products that differ only in something the name leaves out (a scent or flavour, for example) can share a matchKey, and matchConfidence does not account for that. Check unitPrice and packSize before relying on a name-only join.

matchKey changed on 4 November 2026. Unit words in names are now singular ("2 Pints" and "2 Pint" give the same key), and pint-sized packs of 1 to 8 pints are written in pints (2.27L, 2272ml and 4 pint all end in 4pt). About 0.3% of keys changed (201 of 73,745 in the 4 October 2026 snapshot, mostly milk and products with "litres" or "grams" in the name; this count leaves out Aldi "N Pack" rows, whose matchKey also changes because their pack size is now filled, see the Changelog tab). If you join rows from before that date to rows from after it, join on matchKeyV1, which carries the key exactly as it was built before, on every row for one release; it is then removed, with its own notice. Rows collected before the change, including the free daily dataset's earlier rows, carry the old key in matchKey and have no matchKeyV1.

This Actor is not affiliated with, endorsed by, or partnered with Aldi, Sainsbury's, Asda, Tesco or Morrisons. Retailer names are used only to describe the product data returned.

Free daily dataset: UK grocery price index

A fixed basket of 212 staple products, up to three products per retailer from Tesco, Sainsbury's, Asda and Aldi for each of 20 staples (milk, bread, eggs, butter, cheddar, chicken breast, bananas, apples, potatoes, onions, tomatoes, pasta, rice, baked beans, cereal, coffee, tea bags, sugar, orange juice, olive oil). This Actor is intended to fetch the basket daily in product mode and append the rows to one dataset, so the same products build a price history from 24 September 2026; gaps are listed in the dataset notes. Rows use the same schema as the Actor's own output, including ean, matchKey, loyaltyPrice and scrapedAt; filter or group on scrapedAt for a given day's snapshot.

  • JSON: the dataset with id ynAT9NPps2EdjMOJa, through the Apify API's dataset items endpoint with format=json.
  • CSV: the same dataset and endpoint with format=csv.

No account or token needed, and the dataset does not expire. Apify's dataset items API limit, offset and format query parameters all apply, for example ?format=csv&limit=1000.

This dataset is free to use and cite under CC BY 4.0, which covers our compilation and the facts we record (prices, names, identifiers, pack sizes); attribute it as "Yappman Data". The licence applies to this dataset and the other free index and basket datasets only, not to rows returned by runs of this Actor. Retailer content such as product images, descriptions and ingredient text is not ours and is not licensed by us. Prices are shelf prices as published online by each retailer (no store is chosen for Tesco and Sainsbury's; one fixed store or service point for Asda and Aldi). Intended daily; a day is missing when that morning's run fails. Provided as is; this Actor is not affiliated with the retailers.

Need other products, categories or intraday prices? Run the Actor yourself with your own queries, categoryUrls or productUrls.

Tesco, Sainsbury's, Asda, Aldi and Morrisons: what each retailer exposes

Tesco. We know of no public Tesco price API. This Tesco price scraper reads the same data endpoint Tesco's website uses and returns Tesco prices, the single-item Clubcard price and an EAN barcode on nearly every row. Multibuy and meal-deal Clubcard offers appear only in promotionText, and wasPrice is always null for Tesco. No store is chosen for Tesco.

Sainsbury's. This Sainsbury's price scraper reads the product data Sainsbury's shows online for search and product lookup, and its category pages for category listings, and returns the regular shelf price alongside the Nectar price, with an EAN on nearly every row (codes that fail the check digit are dropped). loyaltyPrice carries that loyalty price while price stays the regular shelf price; other offers, such as 2 for £3, go in promotionText. Sainsbury's own availability flag sets inStock.

Asda. This Asda price scraper is also an Asda price tracker for Rollback deals: wasPrice carries the previous price whenever a Rollback reduction is shown, and promotionText carries multibuys such as Any 2 for £4. An EAN barcode is recovered from Asda's product image id on about 99% of rows; the rest carry none. Stock (inStock) is set per store by the asdaStoreId input (default 4565).

Aldi. This Aldi price scraper returns Aldi's own in-store prices exactly as published; Aldi exposes no barcode anywhere in its listing API, product API or product pages, so rows carry an empty ean array. Every row carries matchKey, a normalised brand, name and pack size, but Aldi sells mostly its own labels and names the brands it shares differently, so a matchKey join rarely finds an Aldi product at another retailer; see the Joining retailers section. Aldi does not publish stock online, so inStock is always null for Aldi rows.

Morrisons (opt-in). This Morrisons price scraper reads the product data Morrisons serves on its groceries site, in search, category, product and catalogue mode (see the Catalogue mode (Morrisons) section). It is not in the default retailers selection; add morrisons to use it. What to know:

  • No EAN. Morrisons publishes no barcode anywhere, so ean is an empty array on every Morrisons row. matchKey (normalised brand + name + pack size) joins only identically named products; see the Joining retailers section.
  • 50 products per listing page. A Morrisons listing page carries product data for only its first 50 products, and later pages repeat the same 50. Category mode therefore expands a category into its sub-categories until each holds 50 products or fewer, and reads each one. A lowest-level category with more than 50 products gives its first 50 and the rest are not returned; the run summary's listingShortfall counts how many products were not reachable that way, so you can see how many products a category run did not reach. Because Morrisons lists reduced products first, the products lost are the ones not on offer. Search returns at most 50 products per query. Catalogue mode fetches the products a category crawl misses from Morrisons' product sitemap (see below).
  • Sponsored products in search. Sponsored products are real products at real prices and are returned, but in search results they come after the search's own results, so a small maxItems is spent on the products that match your query first.
  • Duplicate-looking products. Morrisons sometimes lists the same item under two product ids with slightly different names (for example "British Semi Skimmed Milk 2 Pint" and "... 2 Pints"). Both are returned, as Morrisons treats them as separate products.
  • More Card prices. The More Card price goes in loyaltyPrice; price stays the price any shopper pays. Other offers, such as Buy 2 for £4, go in promotionText.
  • Catchweight items. Items sold by weight (loose fruit, meat, fish) are priced for their typical weight: price is for that weight, packSize shows it (for example 0.25kg), and the per-kilo figure is in unitPrice.
  • "Unbeatable Price" is not a promotion. Morrisons shows an "Unbeatable Price" badge on many products as a standing price-match claim. It is not a time-limited offer, so it is left out of promotionText.
  • Unit prices are returned in the same measures as the other retailers (l, kg, each), converted to pounds where Morrisons publishes pence.

Catalogue mode (Morrisons)

catalogue mode reads the retailer's category tree and product pages in one run, with no queries or URLs. It is available for Morrisons only today. If you select other retailers in catalogue mode they are skipped, and the run summary says catalogue mode is not available for <retailer> yet.

{
"mode": "catalogue",
"retailers": ["morrisons"],
"maxItems": 100
}

This example stops at 100 products, at most 100 × $0.0025 + $0.005 = $0.255 on the Free plan. maxItems: 0 removes the cap: the 29 September 2026 check run returned about 26,000 rows, about $66 on the Free plan.

  • Size and time. A check run on 29 September 2026 returned 26,452 distinct priced grocery rows (606 of them with a More Card price) in 3 hours 41 minutes, with no blocks or challenges. A catalogue run takes about 3 to 4 hours, so set the run timeout to 4 hours (14,400 seconds) when you start it; the default timeout is too short.
  • Cost. Rows are charged per product as usual, with no new event: at the Free-plan price of $0.0025 per product, about 26,000 rows is roughly $66. maxItems caps the rows and so the cost; Apify's maximum-charge setting on the run caps it too.
  • How it works. The Actor crawls Morrisons' category tree, then fetches the product page of each product in Morrisons' product sitemap that the crawl did not reach, which covers the products the 50-per-listing limit hides. Products under Morrisons' known non-grocery departments are read to avoid fetching them again but are not returned or charged; an unrecognised department is treated as grocery and logged, and a sitemap product whose page names no department is kept. The run summary reports listingShortfall, tailFetched, tailNotFound (listed in the sitemap but delisted since) and tailNonGrocery.
  • Other modes are unchanged. search, category and product mode work as before for every retailer.

Grocery price tracking and other use cases

  • Price-comparison apps and deal-tracking channels
  • Competitor and retail-analytics price monitoring
  • Inflation trackers and academic basket-of-goods research
  • AI agents doing UK grocery price or basket lookups

Price a basket in one call

Send a basket in one HTTP call and get every retailer's price for each line, totals per retailer, the cheapest retailer and the price spread between them — prices from the latest price snapshot by default (see snapshot.week), or refetched from each retailer at request time on request.

How to call it

Call POST /basket on the Actor's Standby URL, shown on the Endpoints tab, and authenticate with your Apify API token as an Authorization: Bearer <token> header:

curl -X POST "https://<standby-url>/basket" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"homeRetailer": "tesco",
"items": [
{ "ean": "5000181024050" },
{ "name": "semi skimmed milk", "packSize": "2.27L" },
{ "retailer": "tesco", "retailerProductId": "254656543" }
],
"retailers": ["tesco", "sainsburys", "asda", "aldi"],
"live": false,
"includeLoyalty": true
}'

Replace <standby-url> with the URL shown on this Actor's Endpoints tab in Console; it is the same for every caller. Keep your token out of anything you paste or commit; pass it as an environment variable such as $APIFY_TOKEN, never paste the token itself.

The response prices every line at every requested retailer, then compares across retailers. Abbreviated below to one retailer, one priced line and one unmatched line:

{
"market": "uk",
"currency": "GBP",
"live": false,
"snapshot": { "week": "2026-W40", "pricedAt": "2026-10-04T03:12:00Z", "ageDays": 2, "stale": false },
"retailers": [
{
"retailer": "tesco",
"available": true,
"lines": [
{
"index": 0,
"matched": {
"retailerProductId": "257265436",
"ean": ["5000181024050"],
"name": "Cravendale Filtered Fresh Whole Milk 2L",
"price": 2.65,
"loyaltyPrice": null,
"matchedBy": "ean",
"confidence": 1.0
},
"priceSource": "snapshot"
}
],
"unmatched": [ { "index": 1, "reason": "not-found" } ],
"totals": { "shelf": 2.65, "loyalty": 2.65, "matchedLines": 1, "unmatchedLines": 1 }
}
],
"cheapestRetailer": "aldi",
"spreadPct": 12.5,
"comparedLines": 2,
"charges": { "basket-line": 6, "basket-line-live": 0 }
}

IDs and EANs above are illustrative, not real product identifiers.

Request fields

FieldTypeDefaultNotes
itemsarrayrequired1–200 lines; each item needs ean, or name (optionally with packSize), or retailer + retailerProductId — forms can be combined on one item
retailersarray of tesco / sainsburys / asda / aldiall fourWhich retailers to price the basket against. Morrisons is not available on the basket endpoint
homeRetailerone of the abovenoneDefault retailer for items given only as a retailerProductId; that retailer is listed first in the response
livebooleanfalse while a snapshot existstrue refetches the matched lines at request time instead of answering from the snapshot
includeLoyaltybooleantruefalse drops loyaltyPrice and compares retailers on shelf price instead

How matching works

Lines are matched first by retailerProductId at that item's own retailer, then by ean, then by name and pack size: an exact match on our normalised name-and-pack-size key (confidence 0.9), otherwise a fuzzy match (0.85 at most). An ean or retailerProductId match has confidence 1.0. Aldi publishes no barcode, so Aldi lines only ever match by name and pack size. A line that doesn't match at a retailer is listed under that retailer's unmatched, and is never priced or charged. cheapestRetailer and spreadPct are worked out only from the lines matched at every retailer being compared with a confidence of at least 0.8 — comparedLines says how many that was.

Snapshot vs live

By default every price comes from the latest price snapshot: snapshot.week, pricedAt and ageDays say how old it is, and stale turns true once a snapshot passes 10 days old — a stale snapshot is still answered, just flagged. Send "live": true to refetch the matched lines at each retailer instead; refetched prices are then cached for up to 6 hours while your Standby run stays up. If a refetch fails on a line that's in the snapshot, you still get an answer: the snapshot price, marked as such, charged at the snapshot rate.

Limits and errors

Up to 200 items per request; a larger basket is refused outright, never truncated. Live answers stop after about 220 seconds; lines not priced by then come back unmatched as live-timeout, not charged.

  • 400 — bad JSON, or the basket failed validation (an unknown field, more than 200 items)
  • 402 — at least one line matched, but the spending limit Apify sets on your Standby run (you can't change it) doesn't cover any of them
  • 413 — request body over 256 KB
  • 415 — Content-Type other than application/json
  • 429 — two live baskets are already running in your Standby run; retry after 30 seconds
  • 503 — the basket pricing events aren't set up, the snapshot index hasn't loaded, or charging is temporarily unavailable; nothing is charged for lines that come back unmatched — retry
  • 504 — the request ran out of time; nothing was charged

Input reference

FieldTypeDefaultNotes
retailersarray of tesco / sainsburys / asda / aldi / morrisons["aldi","sainsburys"]Which retailers to search. Each runs as its own adapter. morrisons is opt-in: it is not in the default selection. Ignored in category and product mode, where the retailer is inferred from each URL
modesearch / category / product / cataloguesearchOne mode per run. catalogue reads the retailer's category tree and product pages, takes no queries or URLs, and works for Morrisons only today (see the Catalogue mode (Morrisons) section); set a 4-hour run timeout for it
queriesarray of strings["milk"]Search mode; each query runs against every selected retailer
categoryUrlsarray of URLs[]Category mode, up to 250 URLs per run; the retailer is inferred from each URL's hostname. Morrisons: /categories/<slug path>/<id> as copied from the Morrisons groceries site. Tesco: /shop/en-GB/browse/<department>/<aisle>/... as copied from the site, including the /all form (the older /groceries/en-GB/shop/... form works too). Aldi: /products/.../k/<id>. Sainsbury's: /groceries/browse/.../c:<id> (or the legacy /gol-ui/groceries/... form). Asda: /groceries/<category>[/<department>/<aisle>/<shelf>], including top-level pages like /groceries/food-cupboard
productUrlsarray of URLs[]Product mode; retailer inferred from each URL's hostname
maxItemsinteger100Hard cap on rows pushed across all retailers; 0 = no cap, bounded only by mode
asdaStoreIdstring4565Asda store used for stock (inStock). Leave blank for store 4565, the store Asda's website uses before a postcode is entered. We have not checked whether Asda prices change with the store
outputDatasetstring(empty)Advanced: also append every row to this named dataset in your account (created on first use), so repeated runs build history in one place. Rows are charged once, on the run's own dataset. Letters, digits and hyphens only
datasetNamestring(empty)Advanced: at the end of a successful run, name the run's own dataset with this name in your account, so it never expires and is easy to find. Placeholders: {isoWeek} (ISO week of the run start, UTC, e.g. 2026-W40), {date} (YYYY-MM-DD) and {retailer} (the retailer when exactly one ran, otherwise multi). Letters, digits and hyphens only after expansion, max 63 characters, starting with a letter or digit. Skipped with a warning if the name is already taken, nothing was pushed, the run stopped early (spend cap, a failed retailer) or the run is outside the platform, so a retry can take the name

Example inputs

Search three retailers for two terms, capped at 300 rows:

{
"retailers": ["aldi", "sainsburys", "asda"],
"mode": "search",
"queries": ["milk", "cheddar"],
"maxItems": 300
}

One Asda top-level category:

{
"mode": "category",
"categoryUrls": ["https://www.asda.com/groceries/food-cupboard"],
"maxItems": 500
}

Specific product pages:

{
"mode": "product",
"productUrls": [
"https://www.tesco.com/shop/en-GB/products/257265436",
"https://www.sainsburys.co.uk/groceries/product/cathedral-city-mature-cheddar-cheese-350g",
"https://www.aldi.co.uk/product/cowbelle-chocolate-milk-000000000000543221"
]
}

Category snapshot of one retailer, named by ISO week:

{
"mode": "category",
"categoryUrls": ["https://www.tesco.com/shop/en-GB/browse/groceries-and-essentials/fresh-food/milk-butter-and-eggs/milk/all"],
"maxItems": 0,
"datasetName": "snapshot-{retailer}-{isoWeek}"
}

A run started on 4 October 2026 would name this run's dataset snapshot-tesco-2026-W40. maxItems: 0 means no cap, so cost scales with the category. For scale, the Tesco snapshot of 18 October 2026, across many Tesco categories, held 24,533 rows, about $61 on the Free plan at $0.0025 a row. Set maxItems to a small number while you try it.

Output field reference

One row per product per retailer, same shape for all five.

FieldTypeNotes
retailerstringtesco / sainsburys / asda / aldi / morrisons
retailerProductIdstringThe retailer's own id (Tesco tpnc, Sainsbury's product_uid, Asda item id, Aldi SKU, Morrisons product id)
eanarray of stringsCheck-digit-validated GTIN-13 strings; may hold several for multipacks; empty where the retailer exposes none (Aldi, Morrisons, and about 1% of Tesco and Asda rows). Announced for no earlier than 12 November 2026: ean will also leave out retailer-internal numbers such as padded product ids and in-store or coupon codes (see the Changelog tab)
matchKeystringNormalised brand + name + pack size. The join key when ean is empty; it joins only products named the same way. Changed on 4 November 2026 (see joining retailers)
matchKeyV1stringmatchKey as built before 4 November 2026, for joining to older rows; on every row for one release, then removed. matchBasis and matchConfidence describe matchKey, not matchKeyV1
matchBasisstringWhich parts went into matchKey: brand-name-size, name-size, brand-name or name
matchConfidencenumber0 to 1: a fixed value per matchBasis, taken as the conservative lower bound of a one-week benchmark against barcodes (4 October 2026, rechecked on 25 October 2026). It is not a per-row score. name is an assumed value (0.50), not a measured one. Measured on Tesco, Sainsbury's and Asda rows; Aldi and Morrisons rows carry the same per-basis values unmeasured, as neither publishes a barcode
namestringProduct name as shown on site
brandstring or nullOwn-label rows carry the retailer's name
packSizestring or nulle.g. 4x415g, 2.27L
pricenumberGBP, current shelf price
wasPricenumber or nullPrevious price when a reduction is shown
unitPriceobject or null{ "value": 0.99, "measure": "kg" }
loyaltyPricenumber or nullSingle-item Tesco loyalty price (Clubcard), Sainsbury's loyalty price (Nectar) or Morrisons' More Card price; Asda and Aldi leave this null
promotionTextstring or nulle.g. 2 for £3, Any 2 for £4
promotionEndsAtdate or nullISO 8601 when the site exposes it
inStockboolean or nullnull where the retailer does not report stock online (always null for Aldi)
categoryPatharray of stringse.g. ["Dairy", "Milk", "Semi-skimmed"]
urlstringProduct page
imageUrlstring or nullLargest image available. A link to the retailer's own site: the image is the retailer's content, not ours
ratingobject or null{ "value": 4.6, "count": 212 }
scrapedAtstringISO 8601, UTC

Pricing

Pay per event. For batch runs platform usage is included in that price, so this is everything you pay:

  • $0.005 once per run (start fee)
  • $2.50 per 1,000 products returned ($0.0025 each) on the Free plan; higher Apify plans pay less per product: $2.25 per 1,000 at the Bronze tier, $2.00 at Silver and $1.50 at Gold, Platinum and Diamond

Worked example. A run searching 5 terms across all four retailers that returns 300 products costs 300 × $0.0025 + $0.005 = $0.755.

Cap spend with Apify's maximum-charge setting on the run — the Actor stops pushing rows once the cap is reached — and/or with the maxItems input.

POST /basket. Two events, both $0.002 per matched line per retailer ($2.00 per 1,000 lines): basket-line for snapshot answers and basket-line-live for live answers. Unmatched lines are never charged. Batch runs (search, category and product) never charge either basket event. A 50-item basket compared across all four retailers is at most 200 lines, so about $0.40 at most.

Standby calls also carry your own Apify platform usage, on top of the event prices — compute while your Standby run is up, including idle time until it stops — unlike the batch runs above, where platform usage is included in the price. Apify sets each Standby run's spending limit: you can't set one, and it can differ from caller to caller; a call is charged only as far as that limit allows.

Recipes: schedule, webhook and a daily change alert

Run it on a schedule. Save your input as a task in Apify Console, then add a schedule to the task, for example every day at 06:30, and enable it (a new schedule starts disabled). Keep maxItems set so a daily run has a known maximum cost.

React when a run finishes. Add a run-succeeded webhook (ACTOR.RUN.SUCCEEDED) to the task, pointing at your own address. The payload's resource field is the run, and the run's defaultDatasetId inside it names the dataset whose items hold the rows. A webhook for failed runs (ACTOR.RUN.FAILED) or timed-out runs (ACTOR.RUN.TIMED_OUT) tells you when a run did not finish.

Run daily and diff against yesterday. Give each daily run a dataset name with a date in it, for example set datasetName to uk-prices-{date}, so a named dataset survives for each day. Each day, read today's and yesterday's datasets, join the rows on retailer and retailerProductId, and alert when price or loyaltyPrice differs, when wasPrice has appeared, or when a product is in one day and not the other. The Actor itself does not compare days.

Limitations

  • Aldi prices are the in-store reference prices Aldi publishes online; Aldi reports no stock and no barcode for any product.
  • Asda stock (inStock) is for a single store, set by asdaStoreId; we have not checked whether Asda prices change with the store.
  • Sainsbury's rows the retailer itself lists at £0.00 are skipped rather than returned with a fake price.
  • Very large Asda categories are capped by Asda's own search at 1,000 products per department; the Actor splits these automatically, but extremely broad top-level categories may still return only part of the category.
  • No login: no basket, account or delivery-slot data is read, and no Nectar or Asda Rewards account is used.
  • Prices can change during the day; treat scrapedAt as the price's timestamp.
  • Requests: Tesco and Aldi are read at one request a second; search, category and product runs retry a failed request once; if a retailer refuses a request (HTTP 403) the run stops reading that retailer and says so in RUN_SUMMARY.
  • Tesco search results include marketplace-seller and F&F clothing listings; the Actor skips both and returns only Tesco grocery products.
  • No store is chosen for Tesco: its prices and stock (inStock) are as its service shows them with no store selected.
  • Morrisons listing pages give only the first 50 products of a category, so search and category mode can miss products in larger lowest-level categories; listingShortfall in the run summary counts them. Catalogue mode also reads Morrisons' product sitemap.
  • Catalogue mode is available for Morrisons only, takes about 3 to 4 hours (set a 4-hour timeout) and returned about 26,000 rows in the 29 September 2026 check run.

Robots.txt and website terms

The Actor does not log in and does not read robots.txt at run time. robots.txt is not permission and it is not a site's terms. As read on 20 October 2026, the website terms of Tesco and Aldi UK restrict automated access, Asda's terms limit use of its website to browsing and buying (and managing your account) and bar access by commercial organisations, Sainsbury's web-usage terms authorise viewing and downloading its website for personal use only, and we found no automated-access wording in the Morrisons terms. We make no claim that any retailer permits this use. This is not legal advice. You are responsible for your use of the data under those terms.

FAQ

Is there a Tesco API for prices? None that we know of. This Tesco price scraper reads the endpoint Tesco's website uses (since v0.2), in search, category and product mode. It returns an EAN on nearly every row, and single-item loyalty pricing in loyaltyPrice (multibuy and meal-deal offers appear in promotionText instead).

Does it return Clubcard and Nectar prices? Yes. loyaltyPrice carries the single-item Clubcard and Nectar prices for Tesco and Sainsbury's; Asda and Aldi leave the field null.

Can I track Asda Rollback prices? Yes. wasPrice carries the was-price whenever a reduction is shown, and promotionText carries multibuys such as Any 2 for £4.

Does it include Aldi prices? Yes, in search, category and product mode, exactly as Aldi publishes them online. Aldi has no barcode, and its products rarely share an exact name with another shop's, so compare Aldi by unitPrice or with the basket endpoint.

Why do Aldi rows have no EAN? Aldi does not publish a barcode anywhere in its listing API, product API or product pages. Aldi rows carry an empty ean array and a matchKey (normalised brand + name + pack size) instead.

Does it include Morrisons prices? Yes, as an opt-in retailer: add morrisons to retailers. It works in search, category, product and catalogue mode, with the More Card price in loyaltyPrice and no barcode, so matchKey is the only join key and joins only identically named products. See the Morrisons notes above for the 50-products-per-listing limit.

Can I read Morrisons' category tree and product pages in one run? Yes, with catalogue mode on Morrisons: about 26,000 rows in 3 to 4 hours in the 29 September 2026 check run (set a 4-hour timeout), charged per product as usual. Cap it with maxItems.

How do I match the same product across retailers? Use ean where present (Tesco, Sainsbury's and Asda on about 99% of rows or more). Where it's missing, use matchKey and check matchConfidence. For Aldi and Morrisons, which publish no barcode, compare by unitPrice or use the basket endpoint; see the Joining retailers section.

Can I build a supermarket price comparison with it? Yes. Match rows on ean, then matchKey, across Tesco, Sainsbury's and Asda, and compare Aldi and Morrisons by unitPrice, to build supermarket price comparison data in one schema.

Which Asda store's stock does this use? Whichever store id you set in asdaStoreId (default 4565, the store Asda's website uses before a postcode is entered). We have not checked whether Asda prices change with the store.

How do I limit what I spend? Set Apify's maximum-charge cap on the run, and/or set maxItems to cap the number of rows returned.

How fresh is the data? Each row is read when the run reaches it; scrapedAt records when. Prices can move during the day, so treat each row as a snapshot.

Is there a free sample or historical data? Yes. See the Free daily dataset: UK grocery price index section above: a fixed 212-product basket across all four retailers, intended daily since 24 September 2026, no account or token needed.

Does this use my Sainsbury's or Asda account? No. The Actor does not log in and reads no basket or account information.

Can I compare a whole basket across Tesco, Sainsbury's, Asda and Aldi? Yes — see the Price a basket in one call section above. Send a basket to POST /basket on the Actor's Standby URL and get every retailer's price for each line, totals per retailer, the cheapest retailer and the spread, in one call.

Ratings

Ratings and reviews on the Store page help other buyers judge whether this Actor fits their use, whatever your experience.

Changelog

See the Changelog tab for version history.

About

From Yappman Data, a trading name of Abley-Yapp Consulting Limited. Contact: info@yappman.com.