UK Grocery Prices: Tesco, Sainsbury's, Asda, Aldi, Morrisons
Pricing
from $1.50 / 1,000 products
UK Grocery Prices: Tesco, Sainsbury's, Asda, Aldi, Morrisons
Prices for 5 UK supermarkets (Tesco, Sainsbury's, Asda, Aldi, Morrisons) in one schema: price, was-price, Clubcard/Nectar/More Card price, EAN where published, promotion, stock, scrapedAt. Use it to compare a product or basket across shops. Pay per product returned plus $0.005 a run.
Pricing
from $1.50 / 1,000 products
Rating
0.0
(0)
Developer
Russell Yapp
Maintained by CommunityActor stats
0
Bookmarked
27
Total users
6
Monthly active users
a day ago
Last modified
Categories
Share
UK Supermarket Price Scraper: Tesco, Sainsbury's, Asda, Aldi and Morrisons prices API
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).
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 Pricing).
Modes and the questions they answer
| Mode | Use it to answer | Input |
|---|---|---|
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 | "Give me Morrisons' whole range" (Morrisons only; about 26,000 rows in 3 to 4 hours, about $66 on the Free plan, see Catalogue mode) | retailers: ["morrisons"], maxItems: 0 |
POST /basket (Standby) | "What does my shopping list cost at each shop, and which shop is cheapest?" See Price a basket in one call | a basket in the request body |
Three questions to start with:
- "What does semi-skimmed milk cost at Tesco and Aldi today?" (search:
queries: ["semi skimmed milk"],retailers: ["tesco", "aldi"]) - "Which products in this Asda category are on Rollback?" (category: read
wasPriceandpromotionText) - "Which shop is cheapest for this shopping list?" (the basket endpoint)
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://assets.sainsburys-groceries.co.uk/gol/357937/image.jpg","rating":{"value":4.61,"count":2135},"scrapedAt":"2026-10-09T05:30:10Z","matchKey":"sainsburys british semi skimmed milk 2.27l 4 pint 2270ml"}
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.
Run it with the default input
16-second demo on YouTube: https://youtu.be/SYVBAlSUBHY
Sample output row
{"retailer": "sainsburys","retailerProductId": "3449370","ean": ["5000119000201"],"matchKey": "sainsburys british semi skimmed milk 2.27l 4 pint 2270ml","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": ["Dairy, eggs & chilled", "Milk & cream", "Fresh milk"],"url": "https://www.sainsburys.co.uk/groceries/product/sainsburys-british-semi-skimmed-milk-2-27l-4-pint","imageUrl": "https://assets.sainsburys-groceries.co.uk/gol/3449370/image.jpg","rating": { "value": 4.7, "count": 1543 },"scrapedAt": "2026-09-23T18:05:00Z"}
The EAN, matchKey and ids in this sample are illustrative, not real.
What you get: the UK supermarket prices dataset
| Retailer | Modes | Barcode (EAN) coverage | Loyalty / promotion fields | Stock |
|---|---|---|---|---|
| Tesco | search, category, product | On nearly every row (99.1% in the 4 October 2026 snapshot): Tesco's own product API returns a GTIN for almost every product | loyaltyPrice 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 (national, not per store) |
| Sainsbury's | search, category, product | Nearly 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 £3 | Sainsbury's own availability flag |
| Asda | search, category, product | About 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 none | wasPrice carries Rollback was-prices; promotionText carries multibuys, e.g. Any 2 for £4 | Per store, set by the asdaStoreId input (default 4565); prices themselves are national |
| Aldi | search, category, product | None — Aldi publishes no barcode anywhere, so rows carry an empty ean and join on matchKey | wasPrice on in-store markdowns; no loyalty price | Not published — inStock is always null |
| Morrisons (opt-in) | search, category, product, catalogue | None — Morrisons publishes no barcode, so rows carry an empty ean and join on matchKey | loyaltyPrice carries the More Card price; wasPrice and promotionText carry other reductions and offers | Morrisons' own availability flag |
Every row also carries matchKey, a normalised brand + name + pack size, so you can join Aldi and Morrisons products to the others even without a barcode.
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 public 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 fetches the basket every morning in product mode and appends the rows to one dataset, so the same products build a price history from 24 September 2026. 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:
https://api.apify.com/v2/datasets/ynAT9NPps2EdjMOJa/items?format=json - CSV:
https://api.apify.com/v2/datasets/ynAT9NPps2EdjMOJa/items?format=csv
No account or token needed, and the link does not expire. Apify's dataset items API limit, offset and format query parameters all apply, for example ?format=csv&limit=1000.
Free to use and cite under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/), which covers our compilation and the facts we record (prices, names, identifiers, pack sizes); attribute it as "UK Supermarket Price Scraper (Apify), yappman/uk-supermarket-price-scraper". Retailer content such as product images, descriptions and ingredient text is not ours and is not licensed by us. Prices are national shelf prices as published online by each retailer, collected once a day, 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. There is no public Tesco API for prices, so this Tesco price scraper is the nearest thing: it reads Tesco's own product API 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. Tesco prices and stock (inStock) are both national; there is no store selection for Tesco.
Sainsbury's. This Sainsbury's price scraper reads Sainsbury's API for product data 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), though Asda's prices themselves are national.
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. Instead, every row carries matchKey, a normalised brand, name and pack size, so Aldi products join to the other retailers even without a barcode. 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. It is not in the default retailers selection; add morrisons to use it. What to know:
- No EAN. Morrisons publishes no barcode anywhere, so
eanis an empty array on every Morrisons row. Join onmatchKey(normalised brand + name + pack size), as for Aldi. - 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
listingShortfallcounts how many products were not reachable that way, so you can see how complete a category run was. 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
maxItemsis 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;pricestays the price any shopper pays. Other offers, such asBuy 2 for £4, go inpromotionText. - Catchweight items. Items sold by weight (loose fruit, meat, fish) are priced for their typical weight:
priceis for that weight,packSizeshows it (for example0.25kg), and the per-kilo figure is inunitPrice. - "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 a retailer's whole grocery range 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": 0}
- 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 and no proxy. 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.
maxItemscaps 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) andtailNonGrocery. - Other modes are unchanged.
search,categoryandproductmode 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 — snapshot prices from the weekly catalogue by default, or live from each retailer 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": "5000119000201" },{ "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 written out in full.
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": ["5000119000201"],"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
| Field | Type | Default | Notes |
|---|---|---|---|
items | array | required | 1–200 lines; each item needs ean, or name (optionally with packSize), or retailer + retailerProductId — forms can be combined on one item |
retailers | array of tesco / sainsburys / asda / aldi | all four | Which retailers to price the basket against. Morrisons is not available on the basket endpoint |
homeRetailer | one of the above | none | Default retailer for items given only as a retailerProductId; that retailer is listed first in the response |
live | boolean | false while a snapshot exists | true refetches the matched lines live instead of answering from the snapshot |
includeLoyalty | boolean | true | false 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 weekly catalogue 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; live prices are then cached for up to 6 hours while your Standby run stays up. If a live 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-Typeother thanapplication/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
| Field | Type | Default | Notes |
|---|---|---|---|
retailers | array 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 |
mode | search / category / product / catalogue | search | One mode per run. catalogue reads a retailer's whole range, takes no queries or URLs, and works for Morrisons only today (see Catalogue mode); set a 4-hour run timeout for it |
queries | array of strings | ["milk"] | Search mode; each query runs against every selected retailer |
categoryUrls | array 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 groceries.morrisons.com. 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 |
productUrls | array of URLs | [] | Product mode; retailer inferred from each URL's hostname |
maxItems | integer | 100 | Hard cap on rows pushed across all retailers; 0 = no cap, bounded only by mode |
asdaStoreId | string | 4565 | Asda store used for stock (inStock). Leave blank for store 4565, the store asda.com uses before a postcode is entered. Asda prices are national, so this does not change prices |
outputDataset | string | (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 |
datasetName | string | (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"]}
Weekly 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.
Output field reference
One row per product per retailer, same shape for all five.
| Field | Type | Notes |
|---|---|---|
retailer | string | tesco / sainsburys / asda / aldi / morrisons |
retailerProductId | string | The retailer's own id (Tesco tpnc, Sainsbury's product_uid, Asda item id, Aldi SKU, Morrisons product id) |
ean | array of strings | Check-digit-validated GTIN-13 strings; may hold several for multipacks; empty where the retailer exposes none (Aldi, Morrisons, and about 1% of Asda rows) |
matchKey | string | Normalised brand + name + pack size. The join key when ean is empty, and the recommended way to match the same product across retailers |
name | string | Product name as shown on site |
brand | string or null | Own-label rows carry the retailer's name |
packSize | string or null | e.g. 4x415g, 2.27L |
price | number | GBP, current shelf price |
wasPrice | number or null | Previous price when a reduction is shown |
unitPrice | object or null | { "value": 0.99, "measure": "kg" } |
loyaltyPrice | number or null | Single-item Tesco loyalty price (Clubcard), Sainsbury's loyalty price (Nectar) or Morrisons' More Card price; Asda and Aldi leave this null |
promotionText | string or null | e.g. 2 for £3, Any 2 for £4 |
promotionEndsAt | date or null | ISO 8601 when the site exposes it |
inStock | boolean or null | null where the retailer does not report stock online (always null for Aldi) |
categoryPath | array of strings | e.g. ["Dairy", "Milk", "Semi-skimmed"] |
url | string | Product page |
imageUrl | string or null | Largest image available |
rating | object or null | { "value": 4.6, "count": 212 } |
scrapedAt | string | ISO 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 — discounts on higher Apify plans
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, and proxy traffic — 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.
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 byasdaStoreId; Asda prices themselves are national. - 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 not return every product.
- Public, logged-out data only: no basket, account or delivery-slot data, and no Nectar or Asda Rewards account is used.
- Prices can change during the day; treat
scrapedAtas the price's timestamp. - Tesco search results include marketplace-seller and F&F clothing listings; the Actor skips both and returns only Tesco grocery products.
- Tesco prices and stock (
inStock) are both national; there is no store selection for Tesco. - 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;
listingShortfallin the run summary counts them. Catalogue mode is the way to get the whole range. - Catalogue mode is available for Morrisons only, takes about 3 to 4 hours (set a 4-hour timeout) and returns about 26,000 rows.
FAQ
Is there a Tesco API for prices?
No. Tesco does not publish one; this Tesco price scraper is the nearest thing, reading Tesco's own product API 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: no barcode, so join on matchKey instead of ean.
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 join on matchKey. See the Morrisons notes above for the 50-products-per-listing limit.
Can I get Morrisons' whole range?
Yes, with catalogue mode on Morrisons: about 26,000 rows in 3 to 4 hours (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 (Sainsbury's on nearly every row, Asda on about 99%). Where it's missing, or to match Aldi and Morrisons products to the others, use matchKey.
Can I build a supermarket price comparison with it?
Yes. Match rows on ean (or matchKey for Aldi) across Tesco, Sainsbury's, Asda and Aldi 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.com uses before a postcode is entered). Asda's prices are national and don't 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?
Every row is scraped live during the run; 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 Free daily dataset: UK grocery price index above: a fixed 212-product basket across all four retailers, fetched daily since 24 September 2026, no account or token needed.
Does this use my Sainsbury's or Asda account? No. The Actor only reads public, logged-out product data — no login, basket or account information.
Can I compare a whole basket across Tesco, Sainsbury's, Asda and Aldi?
Yes — see Price a basket in one call 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.
Reliability
Data-quality figures for this Actor's data: https://data.yappman.com/data-quality
Integrations
- Track weekly UK grocery EAN price changes with Apify, Google Sheets and Gmail (n8n template): prices a fixed list of EANs once a week through
POST /basket, appends rows to a Google Sheet, compares them with the previous week and emails only the items that moved by more than a threshold you set.
Changelog
See ./CHANGELOG.md for version history.
Read more
- How to get Tesco prices with an API in 2026 (dev.to): Tesco and Clubcard prices as structured data, with a worked example.
- Comparing UK supermarket prices programmatically: Tesco, Sainsbury's, Asda and Aldi in one schema (dev.to): joining the retailers on
eanandmatchKey, effective loyalty prices, and the free basket and shelf-price index datasets - UK Supermarket Prices: Daily 212-Item Basket (Kaggle, CC BY 4.0): a monthly snapshot of the free daily dataset above, as a CSV.
Need something custom?
I build and maintain Actors for retail pricing, property listings and sites behind Akamai or Cloudflare, plus scheduled data pipelines with monitoring. Fixed-scope work through Abley-Yapp Consulting — email info@yappman.com with the site and the fields you need.