<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Russell Yapp</title>
    <description>The latest articles on DEV Community by Russell Yapp (@yappman).</description>
    <link>https://dev.to/yappman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4141609%2Fb993867c-5235-488f-b905-284f38e7a57b.png</url>
      <title>DEV Community: Russell Yapp</title>
      <link>https://dev.to/yappman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yappman"/>
    <language>en</language>
    <item>
      <title>What a data-quality page for scraped price data looks like (and the numbers behind ours)</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Tue, 13 Oct 2026 12:25:28 +0000</pubDate>
      <link>https://dev.to/yappman/what-a-data-quality-page-for-scraped-price-data-looks-like-and-the-numbers-behind-ours-378h</link>
      <guid>https://dev.to/yappman/what-a-data-quality-page-for-scraped-price-data-looks-like-and-the-numbers-behind-ours-378h</guid>
      <description>&lt;p&gt;A scraper's README usually ends with a field list: &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;unitPrice&lt;/code&gt;, &lt;code&gt;ean&lt;/code&gt;, &lt;code&gt;inStock&lt;/code&gt;, and so on. It tells you what a row can hold. It does not tell you how often the field is filled, whether a join between two shops lands on the same product, or whether last Tuesday's run worked at all. Those are the questions that decide whether the data is usable, and a field list cannot answer them.&lt;/p&gt;

&lt;p&gt;So for the scrapers I run on Apify (UK supermarkets, Australian supermarkets, Google Hotels prices and UK property listings) I published &lt;a href="https://data.yappman.com/data-quality/" rel="noopener noreferrer"&gt;a data-quality page&lt;/a&gt; with five measures per data source, each with its sample and date. This post explains what each measure catches and what ours show, including the parts that are not flattering.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five measures
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Identifier coverage.&lt;/strong&gt; The share of rows that carry a barcode (EAN/GTIN), or for sources with no barcode, the identifier that plays the same role. It catches the difference between "the schema has an &lt;code&gt;ean&lt;/code&gt; field" and "you can actually join on it". In the 4 October 2026 weekly snapshot, Tesco rows carried a barcode on 99.1% of 23,352 rows, Sainsbury's on 100.0% of 26,511 and Asda on 98.9% of 20,196. Aldi and Morrisons publish no barcodes at all, and the page says so rather than leaving a blank. For hotels the identifier is Google's hotel id, which every row carries by construction, so the page says that is not a measured figure. For property it is the UPRN, which only one portal gives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cross-retailer match accuracy, with its method.&lt;/strong&gt; If you join the same product across shops, how often is the join right? Ours is measured on every pair of rows from two different UK retailers with an identical &lt;code&gt;matchKey&lt;/code&gt; (a normalised brand, name and pack size) in the same 4 October snapshot: 4,251 pairs. Where both rows carry a barcode (4,225 pairs), the pair is scored by whether the barcodes agree: 4,124 do, 97.6%. That is conservative, because a product listed under a second barcode at one shop counts as wrong. The 127 pairs a barcode cannot settle were read by hand on brand, name, pack size and price; counting those, 4,228 of 4,251 are the same product, 99.5%.&lt;/p&gt;

&lt;p&gt;The caveat matters as much as the number: exact &lt;code&gt;matchKey&lt;/code&gt; joins find only about 14.8% of the pairs that a barcode proves are the same product. Shops word the same product differently. So the key is precise but finds few matches, and an accuracy figure without that caveat would mislead anyone planning a join on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Run success.&lt;/strong&gt; The share of scheduled canary runs that succeeded over a stated window. For UK grocery that is 36 runs from 25 September to 13 October 2026 from two daily canaries, all 36 marked SUCCEEDED. Two things about that definition are stated on the page. A run counts as succeeded when the platform marks it SUCCEEDED, so warnings inside a succeeded run are not counted as failures. And a canary is a small smoke test: it shows that a run can reach its sources, not that every field is complete. The hotels canary shows what a real failure looks like in this measure: 13 of 14 runs (92.9%), with the 1 October run failing on a place lookup, fixed that week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Per-field completeness.&lt;/strong&gt; The share of rows with a value for each field that matters. For the 73,745 UK rows in the 4 October snapshot, shelf price is 100.0%, unit price 99.6% and pack size 89.9%. The per-retailer split is where it gets useful: pack size is 100.0% for Asda but 84.1% for Tesco and 86.9% for Sainsbury's, and Aldi's unit price is 94.1%. An overall figure hides which shop is short.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Known gaps, stated plainly.&lt;/strong&gt; The limits a buyer would otherwise find the hard way, in one list per source.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers turned up
&lt;/h2&gt;

&lt;p&gt;Writing the page down forced some findings into the open.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aldi rows mostly cannot be joined automatically.&lt;/strong&gt; Aldi publishes no barcodes and no stock status, and its products rarely share an exact name with another retailer's. The page's advice is to compare Aldi by unit price instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Woolworths stock is always true.&lt;/strong&gt; In a 7,194-row Woolworths sample (store 1101, 7 October 2026), stock status is present on 100% of rows, but every value is true, because the listing shows only available products. A completeness figure alone would have called that field perfect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zoopla's UPRN depends on the listing type.&lt;/strong&gt; With details on, 47% of rows (71 of 150) carried a UPRN in a London for-sale sample of 24 September 2026 that included 67 new-homes listings, against 70% (7 of 10) in a separate 10-listing sample with new homes excluded. Both are small, dated samples, and the page says so; the known gap is that UPRN and EPC rating are often missing on new-homes listings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hotels &lt;code&gt;bookingUrl&lt;/code&gt; is null by design.&lt;/strong&gt; Listing it as a known gap is cheaper than answering the same question from every new user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Morrisons has no barcode measure&lt;/strong&gt;, because it publishes none. The page shows that as a stated "None", not a missing number.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the numbers are produced
&lt;/h2&gt;

&lt;p&gt;Three sources, no estimates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Weekly snapshot files.&lt;/strong&gt; For UK grocery, barcode coverage, completeness and match accuracy are counted from the latest complete weekly snapshot, one row per product per retailer, with the week and date stated next to each figure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canary run metadata.&lt;/strong&gt; Run success is read from the scheduled canaries' run records: the platform status, the dates and the count, nothing inferred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand checks.&lt;/strong&gt; Where a measure needed a person, as with the 127 match pairs a barcode could not settle, the sample size and the method are stated with the figure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every figure is dated, and where there is no measurement the row is left out rather than filled with a guess. The page carries its last-updated date at the top (13 October 2026 for the figures here); each grocery figure names the weekly snapshot it came from, so check both before quoting one.&lt;/p&gt;

&lt;p&gt;If you want to see the raw material, the free daily UK basket is on Kaggle with &lt;a href="https://www.kaggle.com/code/russellyapp/uk-supermarket-basket-a-first-look" rel="noopener noreferrer"&gt;a first-look notebook&lt;/a&gt;, and the scraper itself is &lt;a href="https://apify.com/yappman/uk-supermarket-price-scraper" rel="noopener noreferrer"&gt;the UK Supermarket Price Scraper on Apify&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading anyone's quality claims
&lt;/h2&gt;

&lt;p&gt;When a data vendor, including me, tells you the data is accurate, ask four things: accurate on what sample, measured how, as of when, and what does the method miss? A match rate with no recall figure, a success rate with no definition of success, or a completeness figure with no per-source split is a number you cannot use yet. The useful version is usually less impressive and much more helpful.&lt;/p&gt;

&lt;p&gt;Prices are online shelf prices as published by each retailer on the day shown; they are not store-specific unless stated. This article is not affiliated with, endorsed by, or partnered with any of the retailers, portals or sites named; their names are used only to describe public data, and product names, images and descriptions remain theirs.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>webscraping</category>
      <category>opendata</category>
      <category>data</category>
    </item>
    <item>
      <title>29 pack-size changes in a week: what a shrinkflation scan of 71,000 UK grocery products found</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:05:59 +0000</pubDate>
      <link>https://dev.to/yappman/29-pack-size-changes-in-a-week-what-a-shrinkflation-scan-of-71000-uk-grocery-products-found-4ca2</link>
      <guid>https://dev.to/yappman/29-pack-size-changes-in-a-week-what-a-shrinkflation-scan-of-71000-uk-grocery-products-found-4ca2</guid>
      <description>&lt;p&gt;Shrinkflation is easy to suspect and hard to prove: a pack gets smaller, the price stays put, and the only trace is a number on the label. I wanted to know how often it shows up in UK supermarket data in a single week, so I compared two weekly snapshots of the online catalogues of Tesco, Sainsbury's, Asda and Aldi, taken on 27 September and 4 October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the scan found
&lt;/h2&gt;

&lt;p&gt;About 71,000 products were on sale at the same retailer in both weeks (Tesco 22,574, Sainsbury's 25,369, Asda 19,868, Aldi 3,588). Of those, 29 changed their pack-size text between the two snapshots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;6 got smaller while the shelf price held or rose (the shrinkflation pattern);&lt;/li&gt;
&lt;li&gt;3 got larger;&lt;/li&gt;
&lt;li&gt;5 were the same pack written differently (175g becoming 0.18kg, for example);&lt;/li&gt;
&lt;li&gt;15 could not be compared, usually because one week gave a count ("8 pack") and the other a weight, or no size at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of the six, three were confirmed on the retailers' own product pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Asda, Kenco Millicano instant coffee, both jars.&lt;/strong&gt; The Decaf and the Original (renamed Americano) went from 100g to 90g, and the shelf price from £3.50 to £4.00. Per kilo that is £35.00 to £44.44, up 27%. Asda shows the £4.00 as a Rollback price, with a was-price of £6.92.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sainsbury's Cheese &amp;amp; Garlic Stuffed Mushrooms.&lt;/strong&gt; 200g to 190g at an unchanged £2.10. The site still showed the old unit price (£10.50 per kg) the following week; at 190g it works out at about £11.05.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other three need a human. In each the product's name changed along with its size: a seasonal chocolate figure whose listed weight fell with its new name although its unit price implied the same weight both weeks, a multipack whose bar count dropped while its unit price suggests the old count was a labelling error, and a sauce jar replaced under the same product id by a different, smaller kit. Those are exactly the cases an automatic scan gets wrong, so they are flagged rather than reported.&lt;/p&gt;

&lt;p&gt;Three confirmed cuts in 71,000 products is a footnote, not a trend. It is one week of data; the scan runs again each month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;Each retailer's whole online range is captured once a week into a dataset, one row per product with the shelf price, loyalty price, pack size and unit price as the retailer publishes them. The scan pairs each product with itself in the previous week by the retailer's own product id, and falls back to a shared EAN barcode when the id changed. It reads the pack size into grams, millilitres or a count, and treats a change within the rounding of the shorter form as a rewrite, not a new pack. Unit prices are converted to per kilo or per litre, because one retailer switches between per 100g and per kilo from week to week.&lt;/p&gt;

&lt;p&gt;A product is flagged as shrinkflation only when the quantity fell and the shelf price held or rose. Any product whose name changed as well is marked for a hand check, because a new product under an old id is the commonest false alarm. Promotions are kept as they are: a price shown as a Rollback or a loyalty price is reported as such, not removed.&lt;/p&gt;

&lt;p&gt;The same weekly snapshots feed a free index, &lt;a href="https://data.yappman.com/shelf-price-index/" rel="noopener noreferrer"&gt;the UK online shelf-price index&lt;/a&gt;. The newsletter first reported these finds in &lt;a href="https://yappman.substack.com/p/uk-online-shelf-price-index-week-7dd" rel="noopener noreferrer"&gt;the week 41 issue&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Prices are online shelf prices as published by each retailer on the day shown; they are not store-specific. This article is not affiliated with, endorsed by, or partnered with any of the retailers named; their names are used only to describe public product data, and product names, images and descriptions remain the retailers'.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>data</category>
      <category>opendata</category>
    </item>
    <item>
      <title>Coles, Woolworths and Aldi prices in one schema: what a weekly snapshot actually captures</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Wed, 07 Oct 2026 12:39:25 +0000</pubDate>
      <link>https://dev.to/yappman/coles-woolworths-and-aldi-prices-in-one-schema-what-a-weekly-snapshot-actually-captures-5004</link>
      <guid>https://dev.to/yappman/coles-woolworths-and-aldi-prices-in-one-schema-what-a-weekly-snapshot-actually-captures-5004</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Coles, Woolworths and Aldi Australia have three sites, three schemas and no shared product id. We wanted one weekly snapshot of all three in a single row shape, with a key to join rows on. The Australia Supermarket Price Scraper, an Apify Actor, returns exactly that. This post covers what a real weekly snapshot of all three looks like, measured on Wednesday 7 October 2026 (ISO week 2026-W41), and the one hard constraint we hit: Coles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The snapshot schedule
&lt;/h2&gt;

&lt;p&gt;Coles' and Woolworths' weekly specials start on Wednesday, so the snapshot runs every Wednesday, in five slots from 07:00 UTC. This week it made seven runs, all completed: Coles in 2 runs, Woolworths in 4, Aldi in 1.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Retailer&lt;/th&gt;
&lt;th&gt;Runs&lt;/th&gt;
&lt;th&gt;Products&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Woolworths&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;16,430 (7,194 + 2,515 + 5,582 + 1,139)&lt;/td&gt;
&lt;td&gt;947&lt;/td&gt;
&lt;td&gt;No blocks. Store 1101 (the national default) on every run. Everyday Market (third-party marketplace) products excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aldi&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2,560&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;Plain direct connection, no proxy needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coles&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;6,628 (3,350 + 3,278)&lt;/td&gt;
&lt;td&gt;81 to 82 per run&lt;/td&gt;
&lt;td&gt;Stopped at Coles' request limit, as designed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is 25,618 rows in total.&lt;/p&gt;

&lt;h2&gt;
  
  
  The join keys
&lt;/h2&gt;

&lt;p&gt;Every row carries &lt;code&gt;matchKey&lt;/code&gt;: a normalised brand, name and pack size. It is the recommended way to match the same product across all three retailers.&lt;/p&gt;

&lt;p&gt;Rows can also carry &lt;code&gt;ean&lt;/code&gt;, the GTIN-13 barcode, validated by its check digit. Woolworths rows carry one on nearly every row. Aldi publishes no barcode anywhere, so Aldi rows carry an empty &lt;code&gt;ean&lt;/code&gt; array and join on &lt;code&gt;matchKey&lt;/code&gt;. Coles listing rows, which is what the weekly snapshot reads, carry no barcode either; they also join on &lt;code&gt;matchKey&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard constraint: Coles
&lt;/h2&gt;

&lt;p&gt;Coles' bot protection answers about 80 requests per session and then stops. The Actor never evades or rotates around a block. When Coles stops answering, the Actor stops cleanly for Coles and keeps the rows it has already returned.&lt;/p&gt;

&lt;p&gt;So the question was how much of Coles fits into about 80 requests. Two choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The snapshot reads listing pages only: 48 products per page, with no per-product barcode request.&lt;/li&gt;
&lt;li&gt;Categories are ordered staples first: dairy, eggs, bread and bakery, meat, fruit and vegetables, pantry basics, then the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result this week was 6,628 Coles products in 2 runs (3,350 and 3,278, each stopping at 81 to 82 requests as designed). Coles lists 30,609 products, so that is 21.7% of the range, against a planned 22.9%. It is a partial snapshot, and the staples are the part covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  A finding that changed the product
&lt;/h2&gt;

&lt;p&gt;From 6 October, Coles' per-product data pages grew several times over, to about 40 to 50 KB each. A listing page carries 48 products in about 48 KB, roughly 1 KB per product, so a Coles row with a barcode lookup now takes dozens of times the proxy transfer of a listing row.&lt;/p&gt;

&lt;p&gt;So from version 0.1.8, Coles barcode lookup (&lt;code&gt;colesProductDetails&lt;/code&gt;) is opt-in and off by default. Product mode always looks barcodes up. Left off, Coles search and category rows have an empty &lt;code&gt;ean&lt;/code&gt; and a run gets far more Coles products within the limit. Turn it on if you need barcodes and can accept far fewer Coles products per run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a row looks like
&lt;/h2&gt;

&lt;p&gt;One row, from a Coles product page captured on 24 September 2026 (with &lt;code&gt;unitPrice&lt;/code&gt;, &lt;code&gt;categoryPath&lt;/code&gt;, &lt;code&gt;imageUrl&lt;/code&gt; and &lt;code&gt;scrapedAt&lt;/code&gt; illustrative, not real):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retailer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"coles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retailerProductId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1902454"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"9310036048985"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matchKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pauls zymil lactose free full cream milk 2000ml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pauls Zymil Lactose Free Full Cream Milk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pauls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2L"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"wasPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;6.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unitPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"measure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"l"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"loyaltyPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"promotionText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"save $0.95"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"promotionEndsAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inStock"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"categoryPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Dairy, Eggs &amp;amp; Fridge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Milk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Long Life Milk"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.coles.com.au/product/pauls-zymil-lactose-free-full-cream-milk-2l-1902454"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imageUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.productimages.coles.com.au/productimages/..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AUD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"market"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"au"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"storeId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7674"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"onSaleDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scrapedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-24T10:15:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This row has an &lt;code&gt;ean&lt;/code&gt; because it came from a product page. A Coles row from the listing-only snapshot would have an empty &lt;code&gt;ean&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost of the snapshot
&lt;/h2&gt;

&lt;p&gt;The whole weekly snapshot (7 runs, 25,618 rows) cost $0.49 of platform usage. Of that, 0.039 GB of residential proxy ($0.31) went on Coles and Woolworths. Per 1,000 rows: Woolworths about $0.022, Coles listing-only about $0.016, Aldi about $0.010.&lt;/p&gt;

&lt;p&gt;Those are our platform costs, not what you pay. Your price is below.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run it
&lt;/h2&gt;

&lt;p&gt;Search mode, one keyword across all three retailers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"queries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"milk"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Category mode, from a category URL copied from the retailer's site (the retailer is inferred from the URL):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"categoryUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://www.coles.com.au/browse/dairy-eggs-fridge/milk"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Apify's Python client (&lt;code&gt;apify-client&lt;/code&gt; 3.x, where &lt;code&gt;call()&lt;/code&gt; returns a run object), then grouping rows on &lt;code&gt;matchKey&lt;/code&gt; to see one product across retailers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yappman/au-supermarket-price-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;by_product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;by_product&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matchKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retailer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;by_product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pack sizes are written differently across retailers, so a &lt;code&gt;matchKey&lt;/code&gt; join will miss some products that are the same in the shop. Use &lt;code&gt;ean&lt;/code&gt; where it is present.&lt;/p&gt;

&lt;h2&gt;
  
  
  Special Buys
&lt;/h2&gt;

&lt;p&gt;Version 0.1.8 also adds a Special Buys mode (&lt;code&gt;"mode": "specialBuys"&lt;/code&gt;) for Aldi Australia's dated Special Buys drops, which Aldi puts in store on Wednesdays and Saturdays. For example, the 7 October drop was 94 products. Rows carry a new &lt;code&gt;onSaleDate&lt;/code&gt; field with the drop date; the field is &lt;code&gt;null&lt;/code&gt; for Coles and Woolworths rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and limits
&lt;/h2&gt;

&lt;p&gt;Pricing is pay-per-event: $2.00 per 1,000 products returned on the Free tier, $1.20 per 1,000 at Gold, plus $0.005 per run start. Cap spend with Apify's maximum-charge setting on the run, and with the &lt;code&gt;maxItems&lt;/code&gt; input.&lt;/p&gt;

&lt;p&gt;The data is public and logged-out: no login, basket or account data. Woolworths prices are for the store Woolworths assigns the session (usually 1101); there is no store selection. Aldi publishes no barcode and no online stock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;p&gt;The Australia Supermarket Price Scraper is on the Apify Store: &lt;a href="https://apify.com/yappman/au-supermarket-price-scraper" rel="noopener noreferrer"&gt;https://apify.com/yappman/au-supermarket-price-scraper&lt;/a&gt;. This Actor is not affiliated with, endorsed by, or partnered with Coles, Woolworths or Aldi; retailer names are used only to describe the public product data it returns.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>python</category>
      <category>data</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Planning applications and constraints next to UK property listings (Rightmove, Zoopla, OnTheMarket)</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Mon, 05 Oct 2026 16:00:52 +0000</pubDate>
      <link>https://dev.to/yappman/planning-applications-and-constraints-next-to-uk-property-listings-rightmove-zoopla-onthemarket-2kic</link>
      <guid>https://dev.to/yappman/planning-applications-and-constraints-next-to-uk-property-listings-rightmove-zoopla-onthemarket-2kic</guid>
      <description>&lt;p&gt;The Actor "UK Property Listings" returns Rightmove and Zoopla rows, with OnTheMarket as an opt-in third portal, on one schema (every row has a &lt;code&gt;portal&lt;/code&gt; field). It now has two optional planning add-ons for each listing, beside a separate sold-prices mode. This post describes what they return, what they cost and where they stop.&lt;/p&gt;

&lt;p&gt;Store page: &lt;a href="https://apify.com/yappman/uk-property-listings" rel="noopener noreferrer"&gt;https://apify.com/yappman/uk-property-listings&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The base: three portals, one schema
&lt;/h2&gt;

&lt;p&gt;Run it from a pasted search or listing URL, from place names or postcodes with filters, or from a list of listing URLs. &lt;code&gt;portals&lt;/code&gt; defaults to Rightmove and Zoopla; add &lt;code&gt;"onthemarket"&lt;/code&gt; to include OnTheMarket. On OnTheMarket the price, bedroom and type filters are applied by the Actor to the rows it fetches, and a search shows at most 1,020 results. Apify Proxy is used as needed and is included in the price. Runs are capped at 1,000 rows, and &lt;code&gt;maxItems&lt;/code&gt; defaults to 40.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning constraints
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;includePlanningConstraints&lt;/code&gt; looks up official constraints at each listing's coordinates from the UK government's planning data service (planning.data.gov.uk). The fields are &lt;code&gt;conservationArea&lt;/code&gt;, &lt;code&gt;listedBuilding&lt;/code&gt;, &lt;code&gt;floodRiskZone&lt;/code&gt;, &lt;code&gt;greenBelt&lt;/code&gt;, &lt;code&gt;article4&lt;/code&gt; and &lt;code&gt;treePreservationZone&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;England only. Outside England, &lt;code&gt;planning.coverage.constraints&lt;/code&gt; is &lt;code&gt;"not-covered"&lt;/code&gt; and nothing is charged for that listing.&lt;/li&gt;
&lt;li&gt;The datasets are incomplete. A missing constraint is not proof that none exists.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;listedBuilding&lt;/code&gt; is the nearest listed building or structure within 25 m. It does not mean the property itself is listed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Nearby planning applications
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;includePlanningApplications&lt;/code&gt; returns applications near each listing: reference, council, address, description, decision state, type, received and decided dates, distance, and a link to the council's page. The data comes from a nightly copy of PlanIt's UK planning data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;planningRadiusMetres&lt;/code&gt;: 25 to 500, default 150.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;planningYears&lt;/code&gt;: 1 to 3, default 1.&lt;/li&gt;
&lt;li&gt;Up to the 100 nearest are returned, nearest first. &lt;code&gt;planning.coverage.applicationsTruncated&lt;/code&gt; says when there were more.&lt;/li&gt;
&lt;li&gt;Descriptions are the council's text with titled names (Mr, Mrs, Dr) removed. Applicant and agent name fields are never kept.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sold prices
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mode: "sold-prices"&lt;/code&gt; returns HM Land Registry sale history, read via Rightmove's sold-price pages. It is a separate mode from listings, and the planning add-ons do not apply to it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rightmove only, and England and Wales only.&lt;/li&gt;
&lt;li&gt;Postcode level. A row has no street address, listing link or Rightmove id. This is a licensing choice: the addresses attached to Land Registry prices come from Royal Mail's address file.&lt;/li&gt;
&lt;li&gt;Each row has the postcode, the postcode centroid as coordinates, &lt;code&gt;lastSoldPrice&lt;/code&gt;, &lt;code&gt;lastSoldDate&lt;/code&gt; and every recorded sale since 1995, newest first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A worked input
&lt;/h2&gt;

&lt;p&gt;The run behind the output below (the launch build's first smoke run): Leeds, Rightmove and Zoopla, six rows, planning applications only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"locations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Leeds"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"portals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rightmove"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zoopla"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includePlanningApplications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The planning block of one output row, from run &lt;code&gt;XjlFAz1GCf8ihFZou&lt;/code&gt; on build 0.2.8, 5 October 2026 — the row with the most applications, as returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"planning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"applications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"26/00553/TR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"authority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Leeds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rear Of Beechwood House Ling Lane Scarcroft Leeds LS14 3HX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T1 Oak: (located in garden of 8 fern croft scarcroft) Reduce the lateral branches by 2.5m leaving a 2m overhang. The trees canopy is shading the boundary leylandii hedge with early signs the hedge is starting to fail . This would allow more light onto the south facing hedge."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Unresolved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Trees"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"receivedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-29"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"decidedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"distanceMetres"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://publicaccess.leeds.gov.uk/online-applications/applicationDetails.do?keyVal=T9M6L7JBJPA00&amp;amp;activeTab=summary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sourceUpdatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-02T01:52:45Z"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"26/01612/TR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"authority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Leeds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rear Of 10 Fern Chase Scarcroft Leeds LS14 3JL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T1 Beech: To cut back all overhanging branches by 4m to the boundary hedge to suitable growing points."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Unresolved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Trees"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"receivedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"decidedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"distanceMetres"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://publicaccess.leeds.gov.uk/online-applications/applicationDetails.do?keyVal=TC5DN5JBG5Y00&amp;amp;activeTab=summary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sourceUpdatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T02:15:27Z"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"constraints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"conservationArea"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"listedBuilding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"floodRiskZone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"greenBelt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"article4"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"treePreservationZone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"coverage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"applications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"planit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"constraints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not-requested"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"constraintsCoverageNote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"England only; official datasets are incomplete, so absence is not proof of no constraint; listedBuilding means a listed building or structure within 25 m, not that this property is listed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"applicationsCoverageFrom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-07-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"applicationsTruncated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"applicationsNote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PlanIt mirror, copied nightly (decisions on older applications can lag; see sourceUpdatedAt); council text, lightly redacted; covers applications received since applicationsCoverageFrom; applicant and agent names are never kept"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"checkedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-05T15:39:14Z"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To read it: &lt;code&gt;coverage.applications&lt;/code&gt; is &lt;code&gt;"planit"&lt;/code&gt; when the copy answered, &lt;code&gt;"not-covered"&lt;/code&gt; for an area PlanIt does not currently collect, or &lt;code&gt;"unavailable"&lt;/code&gt;; &lt;code&gt;applicationsCoverageFrom&lt;/code&gt; is the date before which no applications are included, and &lt;code&gt;applications&lt;/code&gt; is a list that may be empty. It is &lt;code&gt;null&lt;/code&gt; when applications were not requested, the area is not covered, or the copy was unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Pay per event. Platform usage is included.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Charged when&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;run-start&lt;/td&gt;
&lt;td&gt;Once per run, only when rows arrive&lt;/td&gt;
&lt;td&gt;$0.005 per run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;listing&lt;/td&gt;
&lt;td&gt;Each listing row&lt;/td&gt;
&lt;td&gt;$3.00 per 1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;listing-detail&lt;/td&gt;
&lt;td&gt;Each detail page fetched&lt;/td&gt;
&lt;td&gt;$5.00 per 1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;planning-constraints&lt;/td&gt;
&lt;td&gt;Each listing where the lookup succeeds for a point in England&lt;/td&gt;
&lt;td&gt;$5.00 per 1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;planning-applications&lt;/td&gt;
&lt;td&gt;Each listing where the lookup answers, including "none nearby"; not for not-covered or unavailable&lt;/td&gt;
&lt;td&gt;$10.00 per 1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sold-price rows have their own event, &lt;code&gt;sold-price&lt;/code&gt;, at $3.00 per 1,000.&lt;/p&gt;

&lt;p&gt;Worked example from the README: 200 listings with details and planning constraints is 200 × $0.013 + $0.005, about $2.61.&lt;/p&gt;

&lt;p&gt;"None nearby" is charged, because it is an answer: a listing with no applications in the radius costs the same as one with many. Not charged: a run that returns nothing; a listing outside England for constraints; a listing in an area PlanIt does not currently collect (&lt;code&gt;planning.coverage.applications: "not-covered"&lt;/code&gt;); and an applications lookup when the copy is out of date or cannot be read, or the grid cells around the listing hold no applications from a council that counts (&lt;code&gt;"unavailable"&lt;/code&gt;). Higher Apify plans get tiered discounts, and the maximum charge per run setting caps spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Constraints are England only. Applications follow where PlanIt collects: the councils of the applications in the roughly 1 km of grid cells around a listing decide, and an area whose councils PlanIt no longer collects is answered not-covered, free. Near a council boundary, a collected neighbour's applications in those cells make a listing count as covered. Sold prices are England and Wales.&lt;/li&gt;
&lt;li&gt;Undated applications are left out. Without a received date they cannot be shown to fall inside the window.&lt;/li&gt;
&lt;li&gt;The copy has a start date. Applications received before it are never included, whatever &lt;code&gt;planningYears&lt;/code&gt; is set to. Each answered row carries the date as &lt;code&gt;planning.coverage.applicationsCoverageFrom&lt;/code&gt;. At launch the copy holds about 12 months, and it deepens nightly.&lt;/li&gt;
&lt;li&gt;The newest days arrive a few days late. If the copy's newest complete day is more than 5 days old, the add-on counts the copy as out of date. Decisions on applications received more than two months ago can lag by up to a few weeks; &lt;code&gt;sourceUpdatedAt&lt;/code&gt; shows when each record was last copied.&lt;/li&gt;
&lt;li&gt;Searches are user-directed, with no whole-site crawl mode. Each run is capped at 1,000 rows, and each portal caps a single search: Rightmove and Zoopla at about 1,000 results, OnTheMarket at 1,020.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Planning constraints: public sector information under the Open Government Licence v3.0 (planning.data.gov.uk).&lt;/li&gt;
&lt;li&gt;Planning applications: courtesy of PlanIt (planit.org.uk).&lt;/li&gt;
&lt;li&gt;Sold prices: HM Land Registry data © Crown copyright and database right 2026, under the Open Government Licence v3.0. Postcode centroids contain OS data and Royal Mail data © Crown copyright and Royal Mail copyright and database right 2026, with source Office for National Statistics under the OGL v3.0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Actor is not affiliated with or endorsed by Rightmove, Zoopla or OnTheMarket. You are responsible for your own use of the data.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>apify</category>
      <category>opendata</category>
    </item>
    <item>
      <title>Comparing UK supermarket prices programmatically: Tesco, Sainsbury's, Asda and Aldi in one schema</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:21:21 +0000</pubDate>
      <link>https://dev.to/yappman/comparing-uk-supermarket-prices-programmatically-tesco-sainsburys-asda-and-aldi-in-one-schema-4epi</link>
      <guid>https://dev.to/yappman/comparing-uk-supermarket-prices-programmatically-tesco-sainsburys-asda-and-aldi-in-one-schema-4epi</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Four supermarkets, four sites, four different schemas, and no shared product id between them. If you want to compare Tesco, Sainsbury's, Asda and Aldi prices programmatically, what you actually need is one row shape that fits all four, and a key you can join rows on. We built the UK Supermarket Price Scraper, an Apify Actor, to return exactly that: one schema across all four retailers, with the join keys built in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The join keys
&lt;/h2&gt;

&lt;p&gt;The join key we use is &lt;code&gt;ean&lt;/code&gt;, the GTIN-13 barcode, validated by its check digit. Tesco carries an EAN on every row we have seen. Sainsbury's carries one on nearly every row (codes that fail the check digit are dropped). Asda carries one on about 97% of rows, recovered from Asda's own product image id; the rest have none. Aldi publishes no barcode anywhere in its listing API, product API or product pages, so Aldi rows carry an empty &lt;code&gt;ean&lt;/code&gt; array. Morrisons, added on 5 October 2026 as an opt-in fifth retailer (&lt;code&gt;retailers: ["morrisons"]&lt;/code&gt;), also publishes no barcode, so its rows join on &lt;code&gt;matchKey&lt;/code&gt; like Aldi's.&lt;/p&gt;

&lt;p&gt;Every row, from all four retailers, also carries &lt;code&gt;matchKey&lt;/code&gt;: a normalised brand, name and pack size. It is the join key for Aldi, and the recommended way to match the same product across retailers generally, since every row carries it whether or not an EAN is present.&lt;/p&gt;

&lt;p&gt;One honest caveat: pack sizes are written differently across retailers, for example Asda's "4 pint" against Tesco's "2.272L" for the same four-pint milk. A &lt;code&gt;matchKey&lt;/code&gt; join on own-label milk needs a normalising step of its own to catch that. An &lt;code&gt;ean&lt;/code&gt; join does not, because the barcode identifies the exact product regardless of how its pack size is written on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked comparison
&lt;/h2&gt;

&lt;p&gt;All prices below are national online shelf prices as published on each retailer's own site on 24 September 2026. They will have moved since; treat the table as an illustration of the method, not today's prices.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Tesco&lt;/th&gt;
&lt;th&gt;Sainsbury's&lt;/th&gt;
&lt;th&gt;Asda&lt;/th&gt;
&lt;th&gt;Aldi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Semi-skimmed milk, 4 pints (2.27 L)&lt;/td&gt;
&lt;td&gt;£1.65&lt;/td&gt;
&lt;td&gt;£1.65&lt;/td&gt;
&lt;td&gt;£1.65&lt;/td&gt;
&lt;td&gt;£1.65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Own-label salted butter, 250 g&lt;/td&gt;
&lt;td&gt;£1.85&lt;/td&gt;
&lt;td&gt;£1.85&lt;/td&gt;
&lt;td&gt;£1.85&lt;/td&gt;
&lt;td&gt;£1.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Granulated sugar, 1 kg&lt;/td&gt;
&lt;td&gt;£1.09&lt;/td&gt;
&lt;td&gt;£1.09&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;£1.09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cravendale filtered semi-skimmed milk, 1 L&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;£1.75 (Nectar £1.40)&lt;/td&gt;
&lt;td&gt;£1.64&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free-range large eggs, 6&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;£2.10&lt;/td&gt;
&lt;td&gt;£2.10&lt;/td&gt;
&lt;td&gt;£1.69&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Asda's granulated sugar comes in a 500 g pack at £0.89, not the 1 kg pack the other three sell, so it is left out of the row above rather than compared directly.&lt;/p&gt;

&lt;p&gt;Two things stand out. First, on plain staples the big four price-match each other to the penny: milk, butter and sugar are identical, or near enough, across Tesco, Sainsbury's, Asda and Aldi. The interesting signal is not the staple; it is loyalty pricing and the branded lines, where retailers actually diverge, as the Cravendale and egg rows above show. Second, a comparison has to use the effective price, meaning the loyalty price when one is shown, or it misreads Sainsbury's and Tesco: Sainsbury's £1.75 shelf price for Cravendale becomes £1.40 with a Nectar card, and any Tesco row carrying a Clubcard price in &lt;code&gt;loyaltyPrice&lt;/code&gt; is cheaper than its &lt;code&gt;price&lt;/code&gt; field alone suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code: cheapest price per product, across all four retailers
&lt;/h2&gt;

&lt;p&gt;One run across all four retailers in search mode, then group the rows by &lt;code&gt;ean&lt;/code&gt; (falling back to &lt;code&gt;matchKey&lt;/code&gt; where &lt;code&gt;ean&lt;/code&gt; is empty) and keep the cheapest effective price per product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yappman/uk-supermarket-price-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retailers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tesco&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sainsburys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aldi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semi skimmed milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;effective_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;loyalty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loyaltyPrice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;loyalty&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;loyalty&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;loyalty&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;

&lt;span class="n"&gt;cheapest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matchKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;effective_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cheapest&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cheapest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;cheapest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cheapest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retailer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;effective_price&lt;/code&gt; treats a loyalty price as the real price whenever it is lower than the shelf price. Asda and Aldi rows always fall through to &lt;code&gt;price&lt;/code&gt;, because those two retailers never set &lt;code&gt;loyaltyPrice&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  History and trend
&lt;/h2&gt;

&lt;p&gt;For a longer view than one run gives you, the Actor also publishes a free daily dataset: a fixed basket of 212 staple products, up to three products per retailer 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), across Tesco, Sainsbury's, Asda and Aldi. It is fetched every morning and appended to one dataset, on the same schema as the Actor's own output, so the same products build a price history from 24 September 2026. Filter or group on &lt;code&gt;scrapedAt&lt;/code&gt; for a given day's snapshot.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON: &lt;code&gt;https://api.apify.com/v2/datasets/ynAT9NPps2EdjMOJa/items?format=json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CSV: &lt;code&gt;https://api.apify.com/v2/datasets/ynAT9NPps2EdjMOJa/items?format=csv&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither URL needs an account or token. The same basket is also on Kaggle as a CSV under CC BY 4.0, refreshed monthly: &lt;a href="https://www.kaggle.com/datasets/russellyapp/uk-supermarket-prices-daily-212-item-basket" rel="noopener noreferrer"&gt;UK Supermarket Prices: Daily 212-Item Basket&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For a market-level view rather than a basket, there is also the UK Online Shelf-Price Index: a weekly index of posted online prices at the same four retailers, with the week of 27 September 2026 (2026-W39) as 100. It is published as aggregates only (a headline, each retailer and each product class, with the number of matched products behind every figure). Each week is appended and never revised, and the four-week change appears from W43.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON: &lt;code&gt;https://api.apify.com/v2/datasets/AvRCWqFinCiNjx3sP/items?format=json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CSV: &lt;code&gt;https://api.apify.com/v2/datasets/AvRCWqFinCiNjx3sP/items?format=csv&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It measures posted online shelf prices at these four retailers, not what shoppers pay, and it is not the ONS CPI. The method is written up at &lt;a href="https://data.yappman.com/shelf-price-index" rel="noopener noreferrer"&gt;https://data.yappman.com/shelf-price-index&lt;/a&gt;. One row per index value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;INDEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.apify.com/v2/datasets/AvRCWqFinCiNjx3sP/items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INDEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;retailers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retailer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retailers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pivot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vintage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retailer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A week-on-week change per product, once the rows are in a dataframe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;DATASET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.apify.com/v2/datasets/ynAT9NPps2EdjMOJa/items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATASET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scrapedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scrapedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# Keep the last seven days: from the start of the day a week before the newest run.
&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scrapedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scrapedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scrapedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;weekly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retailer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retailerProductId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;last_price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;weekly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;change&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weekly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;weekly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cost and limits
&lt;/h2&gt;

&lt;p&gt;Pricing is pay-per-event: $0.005 once per run, plus $2.50 per 1,000 products returned ($0.0025 each) on the Free plan, with discounts on higher Apify plans. Cap spend with Apify's maximum-charge setting on the run, and with the &lt;code&gt;maxItems&lt;/code&gt; input. The data itself is public and logged-out: no basket, account or delivery-slot information. Asda's stock flag is for a single store, set by &lt;code&gt;asdaStoreId&lt;/code&gt;, though Asda's prices themselves stay national. Aldi publishes neither stock nor a barcode for any product, which is why its rows join on &lt;code&gt;matchKey&lt;/code&gt; instead of &lt;code&gt;ean&lt;/code&gt;. Morrisons is opt-in for now and will join the default retailers once its daily canaries have run clean for two weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;p&gt;The UK Supermarket Price Scraper is on the Apify Store: &lt;a href="https://apify.com/yappman/uk-supermarket-price-scraper" rel="noopener noreferrer"&gt;https://apify.com/yappman/uk-supermarket-price-scraper&lt;/a&gt;. This Actor is not affiliated with, endorsed by, or partnered with Aldi, Sainsbury's, Asda or Tesco; retailer names are used only to describe the public product data it returns.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>data</category>
      <category>api</category>
    </item>
    <item>
      <title>How to get Google Hotels prices with an API in 2026</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Wed, 30 Sep 2026 08:32:03 +0000</pubDate>
      <link>https://dev.to/yappman/how-to-get-google-hotels-prices-with-an-api-in-2026-4120</link>
      <guid>https://dev.to/yappman/how-to-get-google-hotels-prices-with-an-api-in-2026-4120</guid>
      <description>&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Google has no public API for the prices it shows in Google Hotels. The old hotel-price programmes were closed to new users years ago, and nothing replaced them. If you need those prices as structured data now — for a rate-shopping tool, a travel app, a competitor check or an AI agent — you have two options: build and maintain your own scraper, or call one that is already maintained.&lt;/p&gt;

&lt;p&gt;I built the second option. The Google Hotels Price Scraper is an Apify Actor. You give it a destination and dates, or specific hotels, and it returns one row per property with the lead nightly price, the per-vendor rates behind it (Booking.com, Expedia, Agoda and the rest), star class, guest rating, review count, amenities and coordinates. You call it through Apify's API, its client libraries, or the console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the existing scrapers keep failing
&lt;/h2&gt;

&lt;p&gt;Google's hotel pages don't serve tidy data. Prices, ratings and vendor offers sit in positional arrays, where a field is identified by its index rather than its name. When Google shifts an index, a scraper doesn't crash. It quietly returns the wrong number in the right-looking field, or nothing at all. Keeping one working means fixtures for every response type, golden tests that catch a shifted field, and a daily check that fails loudly when the shape changes.&lt;/p&gt;

&lt;p&gt;I can only speak to what is public. The most-used Google Hotels scraper on the Apify Store showed 22,296 successful runs out of 53,949 in its last 30 days, a 41.3% success rate (checked 28 September 2026). Its description documents one input form while the live form is another, and it leaves the proxy setting for the user to choose. Whatever the cause, it means a large share of runs come back empty, and you pay for the attempt.&lt;/p&gt;

&lt;p&gt;The design here is the opposite. The Actor manages its own network access, so there is no proxy setting to get wrong. A run that gets nothing back fails with a clear status instead of returning placeholder rows. A daily check runs the public build and fails if the row shape changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Actor returns
&lt;/h2&gt;

&lt;p&gt;Each row carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hotelId&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;propertyType&lt;/code&gt;, &lt;code&gt;hotelClass&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rating&lt;/code&gt; and &lt;code&gt;reviewCount&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;address&lt;/code&gt;, &lt;code&gt;latitude&lt;/code&gt;, &lt;code&gt;longitude&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checkIn&lt;/code&gt;, &lt;code&gt;checkOut&lt;/code&gt;, &lt;code&gt;nights&lt;/code&gt;, &lt;code&gt;adults&lt;/code&gt;, &lt;code&gt;rooms&lt;/code&gt; — the searched itinerary, on every row&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;leadPrice&lt;/code&gt; — nightly price, total for the stay, currency, and, with vendor rates on, which vendor offers it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vendorRates&lt;/code&gt; — one entry per vendor with its cheapest total for the stay and how many offers it covers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;amenities&lt;/code&gt; and &lt;code&gt;amenityCodes&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;scrapedAt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prices are pre-tax, as Google's nightly figure is, and they are in the currency you ask for. The Actor never converts them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;A search for hotels in Lisbon for 12 to 14 October 2026, with Apify's Python client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yappman/google-hotels-prices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;searches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;destination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lisbon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkIn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-10-12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkOut&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-10-14&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;includeVendorRates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;lead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;leadPrice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;perNight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same call as one curl request that waits for the run and returns CSV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/yappman~google-hotels-prices/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN&amp;amp;format=csv"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"mode":"search","searches":[{"destination":"Lisbon","checkIn":"2026-10-12","checkOut":"2026-10-14"}],"maxItems":20}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 20-hotel search takes about 10 seconds. Two real rows from a run on 29 September 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;Rating (reviews)&lt;/th&gt;
&lt;th&gt;Per night&lt;/th&gt;
&lt;th&gt;Total, 2 nights&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Independente Principe Real&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4.3 (1,020)&lt;/td&gt;
&lt;td&gt;$120.76&lt;/td&gt;
&lt;td&gt;$242&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lisbon Wine House - Rooms &amp;amp; Suites&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3.7 (125)&lt;/td&gt;
&lt;td&gt;$74.99&lt;/td&gt;
&lt;td&gt;$150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;includeVendorRates&lt;/code&gt; on, the first hotel came back with 16 vendors. The four cheapest for the same two nights:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Per night&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vio.com&lt;/td&gt;
&lt;td&gt;$121&lt;/td&gt;
&lt;td&gt;$242&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;goseek.com&lt;/td&gt;
&lt;td&gt;$121&lt;/td&gt;
&lt;td&gt;$242&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Super.com&lt;/td&gt;
&lt;td&gt;$126&lt;/td&gt;
&lt;td&gt;$253&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evendo&lt;/td&gt;
&lt;td&gt;$134&lt;/td&gt;
&lt;td&gt;$267&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each entry also carries &lt;code&gt;vendorDomain&lt;/code&gt; (the vendor's site, for example &lt;code&gt;super.com&lt;/code&gt;) and &lt;code&gt;offerCount&lt;/code&gt;, the number of Google offers folded into that vendor's cheapest. There is no booking link: Google's links carry session identifiers, so the Actor never passes them on. Use &lt;code&gt;vendorDomain&lt;/code&gt; together with the row's &lt;code&gt;url&lt;/code&gt;, which is the Google hotel page.&lt;/p&gt;

&lt;p&gt;These prices were captured on 29 September 2026 and will have changed by the time you read this. Treat them as illustrative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;Pricing is pay-per-event, and platform usage is included for normal runs. On the Free plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$0.002 per hotel row ($2.00 per 1,000)&lt;/li&gt;
&lt;li&gt;$0.0025 per row that carries vendor rates ($2.50 per 1,000), only when you ask for them&lt;/li&gt;
&lt;li&gt;$0.01 per hotel row in history mode with two or more snapshots&lt;/li&gt;
&lt;li&gt;$0.005 per run as Apify's start fee, for a default 1 GB Actor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Bronze, Silver and Gold plans get 10%, 20% and 30% off the row events. The search above, 20 hotels, costs 20 × $0.002 + $0.005 = $0.045. Five hotels with vendor rates cost 5 × $0.002 + 5 × $0.0025 + $0.005 = $0.0275. A blocked or empty run charges no row events. You can cap spend with Apify's maximum-charge setting and the &lt;code&gt;maxItems&lt;/code&gt; input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate history for tracked stays
&lt;/h2&gt;

&lt;p&gt;Once a week the Actor re-reads a set of tracked hotels and stores a dated lead-price snapshot. The tracked set is 50 cities across the US, UK and Europe, each with two stays: one 14 days out and one 42 days out, for 2 adults and 2 nights. &lt;code&gt;history&lt;/code&gt; mode returns those snapshots for hotels you name, oldest first, with the change since the previous snapshot. It makes no Google request. A hotel and dates that aren't tracked return no row and cost nothing. A free &lt;code&gt;tracked&lt;/code&gt; mode lists exactly which hotel ids and dates are covered, so run that first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No browser is needed and none is used. The Actor reads the same data Google serves to any visitor.&lt;/li&gt;
&lt;li&gt;No personal data. There are no reviewer names and no review text. The tracked list holds hotel ids and stay dates only, and nothing about who runs the Actor.&lt;/li&gt;
&lt;li&gt;It's public, logged-out data: what any visitor sees for those dates and that party size.&lt;/li&gt;
&lt;li&gt;One room per search, because Google ignores the field. Filters on price, rating and star class work; free cancellation and amenity-label filters aren't supported yet.&lt;/li&gt;
&lt;li&gt;Vendor rates work in search mode. Looking up specific hotels by id returns their details without a vendor breakdown.&lt;/li&gt;
&lt;li&gt;Very narrow filters can end a search early. The run summary's &lt;code&gt;stoppedEarly&lt;/code&gt; says when that happened.&lt;/li&gt;
&lt;li&gt;History covers tracked stays only, and no more than 100 hotels per run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;p&gt;The Google Hotels Price Scraper is on the Apify Store as &lt;code&gt;yappman/google-hotels-prices&lt;/code&gt;. It is not affiliated with, endorsed by or partnered with Google; Google and Google Hotels are named only to say where the data comes from. Its terms restrict automated access, so you are responsible for how you use the data. This is not legal advice.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>api</category>
      <category>python</category>
      <category>travel</category>
    </item>
    <item>
      <title>How to get Tesco prices with an API in 2026</title>
      <dc:creator>Russell Yapp</dc:creator>
      <pubDate>Thu, 24 Sep 2026 16:37:53 +0000</pubDate>
      <link>https://dev.to/yappman/how-to-get-tesco-prices-with-an-api-in-2026-4n20</link>
      <guid>https://dev.to/yappman/how-to-get-tesco-prices-with-an-api-in-2026-4n20</guid>
      <description>&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;There is no public Tesco price API today. Tesco ran a developer programme in the past, but it was retired years ago and nothing has replaced it. If you need Tesco's shelf prices as structured data now, for a price-comparison tool, competitor monitoring, an inflation tracker or an AI shopping agent, the practical choices are the same as for any other major UK retailer: scrape tesco.com yourself, or use a maintained scraper that already exposes the data through an API.&lt;/p&gt;

&lt;p&gt;We built the second option. The UK Supermarket Price Scraper is an Apify Actor that reads Tesco, Sainsbury's, Asda and Aldi and returns one schema of structured product rows, callable through Apify's API, its client libraries, or the Apify console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why doing it yourself is expensive to keep working
&lt;/h2&gt;

&lt;p&gt;Keeping a Tesco scraper working is not a one-off job. Tesco's site is protected by a bot-management service, so a plain HTTP client needs serious engineering to get a consistent response. Tesco's front end also changed its URL scheme and rendering during 2026, which breaks any scraper written against the old pages. Clubcard pricing adds a further wrinkle: single-item Clubcard prices and multibuy or meal-deal Clubcard offers look similar on the page, but only the single-item price is a number anywhere in the data, and the rest only ever appears as promotion text, so a naive parser silently drops it. Keeping up with all of that, in practice, means fixtures for every page type, a canary run that catches breakage before customers do, and someone on call for when the site next changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Actor returns for Tesco
&lt;/h2&gt;

&lt;p&gt;The Actor runs in three modes for Tesco: &lt;code&gt;search&lt;/code&gt; (keywords, run against Tesco's own search), &lt;code&gt;category&lt;/code&gt; (a tesco.com browse URL, copied from the site, for example &lt;code&gt;.../shop/en-GB/browse/&amp;lt;department&amp;gt;/&amp;lt;aisle&amp;gt;/...&lt;/code&gt; including the &lt;code&gt;/all&lt;/code&gt; form), and &lt;code&gt;product&lt;/code&gt; (a Tesco product page URL, for example &lt;code&gt;https://www.tesco.com/shop/en-GB/products/257265436&lt;/code&gt;). All three modes return the same row shape.&lt;/p&gt;

&lt;p&gt;For Tesco, each row carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;retailer&lt;/code&gt;: always &lt;code&gt;tesco&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retailerProductId&lt;/code&gt;: Tesco's own product id, the &lt;code&gt;tpnc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ean&lt;/code&gt;: the barcode, present on every Tesco row we have seen&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;brand&lt;/code&gt;, &lt;code&gt;packSize&lt;/code&gt;: as shown on the product page (own-label rows carry &lt;code&gt;Tesco&lt;/code&gt; as the brand)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;price&lt;/code&gt;: the current GBP shelf price&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loyaltyPrice&lt;/code&gt;: the single-item Clubcard price, when one is shown&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;promotionText&lt;/code&gt;: multibuy and meal-deal Clubcard offers, as free text (Tesco's &lt;code&gt;wasPrice&lt;/code&gt; is always null)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inStock&lt;/code&gt;: Tesco's own online availability flag, national rather than per store&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;categoryPath&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;imageUrl&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt;, &lt;code&gt;scrapedAt&lt;/code&gt;: as elsewhere in the schema&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;A search for milk on Tesco, using Apify's Python client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yappman/uk-supermarket-price-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retailers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tesco&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semi skimmed milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loyaltyPrice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same call as a single curl request to Apify's run-and-wait endpoint, returning CSV directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/yappman~uk-supermarket-price-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN&amp;amp;format=csv"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"mode":"search","retailers":["tesco"],"queries":["semi skimmed milk"],"maxItems":50}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two real rows from that search, captured on 24 September 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;tpnc&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Loyalty price&lt;/th&gt;
&lt;th&gt;Promotion text&lt;/th&gt;
&lt;th&gt;EAN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tesco British Semi Skimmed Milk 2.272L, 4 Pints&lt;/td&gt;
&lt;td&gt;254656543&lt;/td&gt;
&lt;td&gt;£1.65&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;5000436589457&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arla LactoFREE Semi Skimmed Milk Drink 2L&lt;/td&gt;
&lt;td&gt;309449739&lt;/td&gt;
&lt;td&gt;£3.15&lt;/td&gt;
&lt;td&gt;£2.50&lt;/td&gt;
&lt;td&gt;£2.50 Clubcard Price&lt;/td&gt;
&lt;td&gt;5000181042139&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All prices here are national online shelf prices as published on tesco.com on 24 September 2026. They will have changed by the time you read this; treat them as illustrative, not current.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;Pricing is pay-per-event, and platform usage is included in it. Two charges apply: $0.005 once per run as a start fee, and $2.50 per 1,000 products returned ($0.0025 each) on the Free plan, with discounts on higher Apify plans. A run that searches 5 terms across all four retailers and returns 300 products costs 300 × $0.0025 + $0.005 = $0.755. You can cap spend with Apify's maximum-charge setting on the run, and with the &lt;code&gt;maxItems&lt;/code&gt; input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This is public, logged-out data, with the same limits as the rest of the Actor. Tesco prices and stock are both national figures; there is no store-level stock and no store selection for Tesco. Tesco's own search mixes in marketplace-seller and F&amp;amp;F clothing listings, and the Actor skips both, returning only grocery products. There is no basket, account or delivery-slot data, and no Clubcard account is used; everything comes from pages Tesco serves to any visitor, logged out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more things worth knowing
&lt;/h2&gt;

&lt;p&gt;If you want a look at real numbers before writing any code, there is a free daily dataset: a fixed basket of 212 staple products across Tesco, Sainsbury's, Asda and Aldi, fetched every morning and appended to one dataset since 24 September 2026, no account or token needed:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://api.apify.com/v2/datasets/ynAT9NPps2EdjMOJa/items?format=csv&lt;/code&gt; (CSV; &lt;code&gt;format=json&lt;/code&gt; gives JSON).&lt;/p&gt;

&lt;p&gt;And if you want an alert rather than a one-off pull, there is an n8n workflow for a daily price-drop alert on a shopping basket, built on this Actor: import &lt;a href="https://gist.github.com/yappmaniac/647953b061d36d3c8e9ddb0e8be78faf" rel="noopener noreferrer"&gt;basket-price-drop-alert.json&lt;/a&gt; into n8n.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;p&gt;The UK Supermarket Price Scraper is on the Apify Store: &lt;a href="https://apify.com/yappman/uk-supermarket-price-scraper" rel="noopener noreferrer"&gt;https://apify.com/yappman/uk-supermarket-price-scraper&lt;/a&gt;. This Actor is not affiliated with, endorsed by, or partnered with Tesco; retailer names are used only to describe the public product data it returns.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>api</category>
      <category>data</category>
    </item>
  </channel>
</rss>
