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.
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. RUN_SUMMARY now also counts Aldi products whose pack size or unit price was empty or not understood (fieldGaps), and, on runs of 100 or more Aldi products outside watchlist mode, warns when a quarter or more are missing. No dataset field, price or event changes.
Aldi "Pack" sizes (4 November 2026)
Aldi products whose size is given as a count of packs (for example "6 Pack", with a unit price per "1 Pack") now have packSize and unitPrice filled: "6 Pack" becomes 6 each, and the unit price has measure each. A pack count is a whole number of 1 or more; "1.5 Pack" and "0 Pack" stay empty. Before this change both fields were empty for these products. For those rows, three more fields follow: matchKey now ends in the pack size (for example "merevale british organic eggs 6 pack 6 each"; for these rows the size repeats the count in the name), and matchBasis and matchConfidence are brand-name-size and 0.97, as the key now holds a size. matchKeyV1 is not affected: for these rows it is built exactly as before, without the pack size ("merevale british organic eggs 6 pack"). In the basket endpoint, a line named without a size can now reach these rows at a confidence of 0.85 instead of 0.90 (the same product is found). Rows whose unit price is given per pack get unitPrice filled the same way, even when their size is not a pack; no other field changes on those rows. No field, price or event is added or removed.
matchKey, matchBasis and matchConfidence (4 November 2026)
Announced 5 October 2026, as no earlier than 4 November 2026. This change is its own build; the default-retailers and ean changes announced below ship separately.
matchKey writes unit words in product names in the singular, so "2 Pints" and "2 Pint" give the same key (pints, litres, grams and kilograms).
matchKey writes pint-sized volumes in pints: a pack within 0.5% of a whole number of UK pints (568.26 ml each) is written as that number of pints, <n>pt. So "2.272L", "2.27L", "2272ml" and "4 pint" all become 4pt; other volumes and weights stay in ml and g as today. Clarified 14 October 2026: pint-sized means 1 to 8 pints (per unit of a multipack, so "2x568ml" becomes 2x1pt); larger volumes such as a 70 L bin bag stay in ml. A few non-milk packs within 0.5% of a pint count also become pints (for example 1.7 L kettles as 3pt); both sides change the same way within new rows; to join to rows from before this change, use matchKeyV1.
For one release, every row also carries matchKeyV1, the key exactly as it was built before this change, so existing joins keep working while you move to the new key. matchKeyV1 is removed in a later release, no earlier than 30 days after 4 November 2026, announced here with its date.
Every row also carries matchBasis (brand-name-size, name-size, brand-name or name: which parts of the row went into its matchKey) and matchConfidence (0 to 1: a fixed value per matchBasis, the conservative lower bound of a one-week benchmark against barcodes on Tesco, Sainsbury's and Asda rows (4 October 2026, rechecked on 25 October 2026); not a per-row score; name is an assumed value, 0.50, not a measured one; Aldi and Morrisons rows carry the same per-basis values unmeasured, as neither publishes a barcode). Two rows that share an EAN are the same product whatever their matchKey.
UK edition only. Apart from matchKey, the temporary matchKeyV1 and the two new fields above, no field, price or event changes.
Announced change: default retailers (from 7 November 2026 at the earliest)
Announced 8 October 2026. This change ships in its own build, no earlier than 7 November 2026, separate from the matchKey and ean builds. Morrisons stays opt-in until then.
A run that does not set retailers searches all five retailers (Tesco, Sainsbury's, Asda, Aldi and Morrisons) instead of Aldi and Sainsbury's.
maxItems is shared equally between the shops that run, so a default call (100 items) returns up to 20 rows from each of five shops instead of up to 50 from each of two. With the default cap it is still at most 100 rows, so the most a default call can cost is unchanged ($0.255 on the Free plan: $0.005 to start and $0.0025 for each of up to 100 products). The same query can now return more rows than before: a query that returned 40 rows between the two shops (up to 50 each) can now return up to 100 rows across five shops (up to 20 each) and cost up to $0.255.
A call with maxItems set to 0 (no limit) and no retailers, or one with a higher cap, reads five shops instead of two and can cost more: every row returned is charged. maxItems applies to the whole run, not to each query, so several queries share the cap.
To keep today's behaviour, set retailers to ["aldi", "sainsburys"]. Runs that name their retailers are not affected.
UK edition only. No field, price or event changes.
Announced change: ean (from 12 November 2026 at the earliest)
Announced 28 October 2026. This change ships in its own build, no earlier than 12 November 2026.
ean no longer carries retailer-internal numbers: a retailer's own product id padded to 13 digits, other codes with six or more leading zeros, in-store and coupon codes (13-digit codes starting 2, codes starting 02, 04 or 05, and codes in the 980 to 999 range) and in-store 8-digit codes starting 2. Manufacturer barcodes are not affected.
Measured on the 4 October 2026 snapshots, the share of rows with no ean goes from 1% to about 3% for Tesco, from 0% to about 16% for Sainsbury's and from 1% to about 3% for Asda. Aldi and Morrisons rows already have none.
Where ean is empty, join on matchKey (see the matchKey entry above).
The weekly shelf-price index does not read ean, so published index values do not change.
The basket endpoint matches ean lines against the cleaned list, so a line that uses a removed number will then be unmatched, and unmatched lines are never charged.
UK edition only. No price or event changes.
Tesco category URLs (16 October 2026)
Tesco moved Fresh Food, Bakery, Frozen Food, Treats & Snacks, Food Cupboard, Drinks, Household and Kiosk under a new top-level category, Groceries & Essentials, so category URLs starting with those names stopped working. They work again: an old URL such as /shop/en-GB/browse/fresh-food/all is resolved under its new parent (the run's warnings say so), and the new form /shop/en-GB/browse/groceries-and-essentials/fresh-food/all works too. A category URL deeper than a shelf (super department, department, aisle, shelf) is now refused for that URL, with zero rows and nothing charged, instead of extra levels being ignored; Tesco's old shelf pages such as Whole Milk no longer exist. No price, event or field changes.
0.2 — 2026-09
Tesco adapter: search, category and product modes, reading the endpoint Tesco's website uses.
ean present on nearly every Tesco row (99.1% in the 4 October 2026 snapshot).
Clubcard prices: single-item Clubcard prices in loyaltyPrice; multibuy and meal-deal Clubcard offers in promotionText.
tesco added to the retailers input.
Pricing unchanged: same per-run start fee and per-product price, no new events.
Sainsbury's category mode (25 Sep 2026): pagination now follows the site's redirect when a category URL has moved, and a category whose page says "page cannot be found" is reported as not found instead of a parse failure (issue #31).
outputDataset input (added 24 Sep 2026): also append every row to a named dataset in your account, so repeated runs build a history in one place. Rows are still charged once.
datasetName input (25 Sep 2026, build 0.2.8): at the end of a successful run, name the run's own dataset with {isoWeek}, {date} and {retailer} placeholders, so a scheduled task's datasets are named and never expire; a taken name, an empty run or a run that stopped early leaves the dataset unnamed with a warning, so a retry can take the name.
Build 0.2.7 (25 Sep 2026) was made from an unreviewed working tree and is superseded by 0.2.8; do not pin it.
categoryUrls accepts up to 250 URLs per run (was 50), so a retailer's whole category list fits in one category-mode run; the per-retailer request budget still bounds the work.
Platform migrations (27 Sep 2026): when Apify moves a run to another server (or a run is resurrected), the Actor reads back what the run's dataset already holds and skips those products, so rows stored before the move are no longer pushed or charged again (issue #41). If that read fails, the run stops rather than risk charging twice. RUN_SUMMARY gains resumedRows.
Sainsbury's category mode (29 Sep 2026): an aisle is paged by its real page size, sponsored grid tiles included, so the Actor no longer asks for a page past the end and reports it as a failure (issue #38). When the last page does come back empty, the aisle simply ends there; RUN_SUMMARY counts that in pagesShort, which is left out when 0. Sponsored products shown in the grid are now returned, once each (issue #39). A department URL (a hub page with no listing) now says to use one of its aisle URLs, and only Sainsbury's real "page not found" page is reported as not found (issue #36).
Morrisons fix (10 Oct 2026): Morrisons began adding an analytics query to its product links on listing pages, which made every Morrisons row fail to build its url, so a Morrisons search or category run returned no Morrisons rows (nothing is charged for a row that is not returned). The adapter now reads past the query; the product url in a row is unchanged and carries no query.
Run summary (10 Oct 2026): a retailer whose products mostly fail to be read (half or more of what it listed) is now marked status: "degraded" in RUN_SUMMARY instead of ok, with a reason, and the run's final status message carries a warning naming it. Rows, prices and events are unchanged.
Free daily dataset: the README now links the dataset's plain read-by-id URL, which needs no account or token and does not expire. The earlier signed link stops working on 8 Oct 2026.
Input form wording (8 Oct 2026): every input field now has a plain description, an example value and, where a field belongs to one mode, a note saying which, so AI agents that use the Actor through MCP fill the form correctly. The maxItems description now says the limit is split equally between the shops that run. No field, default, limit or choice changed.
Morrisons search (8 Oct 2026): sponsored products now come after the search's own results instead of first, so a small maxItems is no longer spent on ads that do not match the query. They are still returned, once each; category and catalogue order is unchanged.
Morrisons (5 Oct 2026, opt-in): morrisons added to the retailers input, but not to the default selection. Modes search, category, product and catalogue. Morrisons publishes no barcode, so rows carry an empty ean and join on matchKey; More Card prices go in loyaltyPrice; catchweight items are priced for their typical weight with the per-kilo figure in unitPrice; the standing "Unbeatable Price" badge is not a promotion and is not in promotionText. A listing page gives only its first 50 products, so category and search mode can miss products in larger categories; RUN_SUMMARY gains listingShortfall (left out when 0) to count them.
Catalogue mode (5 Oct 2026): mode accepts catalogue, which reads the retailer's category tree and product pages with no queries or URLs. Morrisons only for now; other selected retailers are skipped with the reason catalogue mode is not available for <retailer> yet. A check run on 29 Sep 2026 returned 26,452 priced grocery rows in 3 hours 41 minutes with no blocks; set a 4-hour run timeout, and use maxItems to cap rows and cost. RUN_SUMMARY gains tailFetched, tailNotFound and tailNonGrocery (each left out when 0).
Pricing unchanged for Morrisons and catalogue mode: same per-run start fee and per-product price, no new events.
POST /basket on the Actor's Standby URL (October 2026): prices a basket of up to 200 items across Tesco, Sainsbury's, Asda and Aldi in one HTTP call; items match by ean, retailerProductId, or name with an optional packSize, each match carrying matchedBy and confidence; the answer gives totals per retailer plus cheapestRetailer and spreadPct over the lines matched at every compared retailer (comparedLines).
Snapshot by default: prices come from the latest price snapshot, with week, ageDays and stale (true past 10 days); send "live": true to refetch the matched lines from the retailers at request time, cached for up to 6 hours while the Standby run stays up.
Pricing (from 12 Oct 2026): two new events, basket-line (snapshot answers) and basket-line-live (live answers), $0.002 per matched line per retailer; unmatched lines are never charged, and batch runs (search, category and product) never charge either event.
Standby calls also carry the caller's own Apify platform usage — compute while the Standby run is up, including idle time until it stops — unlike batch runs, 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.
An OpenAPI description ships with the Actor.
Limits: up to 200 items per request, refused rather than truncated when over the limit; 429 live-busy when too many live baskets are already running in the Standby run.
Batch runs (search, category and product modes) are unchanged.
0.1 — 2026-09
First public release.
Retailers: Aldi, Sainsbury's, Asda.
Modes: search (keywords run against every selected retailer), category (retailer category page URLs) and product (product page URLs).
Join keys on every row: ean (Sainsbury's on nearly every row, Asda on about 97%) and matchKey (normalised brand + name + pack size, used where no EAN exists and as the cross-retailer join key).
Loyalty and promotion fields: Sainsbury's Nectar prices in loyaltyPrice, Asda Rollback was-prices in wasPrice and multibuy promotions in promotionText.
Per-store Asda stock via the asdaStoreId input.
Pay-per-event pricing: a per-run start fee plus a price per product returned.