UK Fuel Prices Scraper
Pricing
from $0.92 / 1,000 station scrapeds
UK Fuel Prices Scraper
Live UK petrol and diesel pump prices from the fuel retailers' own published feeds. One row per filling station: brand, address, postcode, coordinates and every grade it sells, with the age of the retailer's own data on each row so you can tell current prices from frozen ones.
Pricing
from $0.92 / 1,000 station scrapeds
Rating
0.0
(0)
Developer
Dami's Studio
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Petrol and diesel prices at UK filling stations, read from the files the retailers publish themselves. One row per forecourt: the brand over the door, the address, the postcode, the coordinates, and the price in pence per litre of every grade that site sells. No key, no account, no browser.
- Every row carries the timestamp on the retailer's own file and how many hours old it was when the row was written.
- A retailer whose file has not been refreshed recently is skipped and reported, not quietly mixed in with current prices.
- A retailer whose file is offline gets you a free row saying so, naming the retailer and the HTTP status.
- Grades arrive under one set of field names whatever each retailer calls them.
- Empty input returns a free sample row, so you can look at the shape before spending anything.
Price
$0.92 per 1,000 stations, plus a $0.0015 start fee per run.
It is the same rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.
| Stations | Total cost |
|---|---|
| 100 | $0.0935 |
| 1,000 | $0.9215 |
| 10,000 | $9.2015 |
| 100,000 | $92.0015 |
This Actor runs in 256 MB, which is under a gigabyte, so the start fee is charged exactly once per run and the figures in the table are the whole bill.
A station is charged once, when its row is written. Free rows, meaning the sample and every diagnostic row telling you a retailer did not answer or its file was too old, are not charged and never count towards the total.
What is actually charged
- One
station-scrapedevent per station row written to the dataset. Nothing else is metered per row. - Free: the sample row an empty run returns, and every diagnostic row: a blocked target, a dead URL, a search that matched nothing. Those rows all carry
"charged": false. - Stations dropped by the postcode filter.
- Stations from a retailer whose file was older than your age limit.
- Every diagnostic row: a retailer that did not answer, answered with something that was not a price list, or published no usable stations.
- Duplicate forecourts already seen earlier in the same run.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row.
Input
{"retailers": ["asda","mfg","moto"],"maxItems": 200,"maxFeedAgeHours": 48}
| Field | What it does |
|---|---|
retailers | Which retailers to read, or the single word all. The keys are applegreen, ascona, asda, bp, esso, jet, karan, mfg, morrisons, moto, rontec, sainsburys, sgn, shell, tesco. See the coverage table below for which of those are actually publishing today. |
maxItems | Total stations across all the retailers you chose. They are read in the order you list them, so put the one you care most about first. With all, they are read biggest first. Default 200, hard ceiling 20,000. |
maxFeedAgeHours | How stale a retailer's file may be before its stations are skipped and reported instead of charged. Default 48 hours. This is the setting that decides whether you get today's prices or April's. |
postcodePrefixes | Optional. ["SW","M1","EH12"] returns only stations whose postcode starts with one of those. Stations the filter drops are never charged. |
proxyUrls | Leave empty. It exists only for callers who want the traffic to leave through servers they already pay for, as http://user:pass@host:port. |
Run it with empty input and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.
Output
One row per station. A real row from a real run:
{"ok": true,"charged": true,"recordType": "station","retailer": "Motor Fuel Group","retailerKey": "mfg","brand": "BP","siteId": "gcp6cwwx0tje","address": "MFG Alton, Butts Road, Alton","postcode": "GU34 1LH","latitude": 51.145333,"longitude": -0.979814,"petrolE10": 176.9,"petrolE5": 189.9,"dieselB7": 199.9,"dieselSDV": 215.9,"otherPrices": null,"currency": "GBP","priceUnit": "pence per litre","feedUpdatedAt": "2026-09-20T00:00:01.000Z","feedAgeHours": 6.4,"feedStale": false,"onCmaList": true,"scrapedAt": "2026-09-20T06:24:33.812Z"}
Field notes
retailer: the company publishing the file. Not always the brand on the forecourt: one operator runs sites under several oil-company brands.brand: the brand over the door, as the retailer writes it. Casing is theirs, so you will see bothSHELLandShell.siteId: the retailer's own site id. Useful as a key within one retailer, but it is a geohash of the forecourt and it is not unique. A handful of sites share one, and a forecourt that has changed hands appears under two retailers with the same id.latitude / longitude: null rather than0, 0when the retailer published no real position. Four sites do that today; their address and postcode are still correct.petrolE10: standard unleaded, the 95-octane grade, in pence per litre.petrolE5: super unleaded, 97 octane or higher. Only sites selling two grades of petrol publish it.dieselB7: standard diesel, in pence per litre.dieselSDV: the premium diesel grade. Where a site publishes both, this was the dearer of the two on 1,762 of the 1,777 sites carrying it.otherPrices: null almost always. If a retailer starts publishing a grade code this Actor has not seen, it lands here under that code rather than being dropped.feedUpdatedAt: the timestamp on the retailer's own file, not when you ran this. Retailers publish it without a time zone; it is read as UTC.feedAgeHours: how old that timestamp was when the row was written. This is the number to look at before you trust a price.feedStale: always false on a charged row under the default settings. It can only be true if you raisedmaxFeedAgeHoursto let older files through on purpose.onCmaList: whether the retailer was on the government's published list of participating retailers in its final revision.
Every real row carries "charged": true. Sample rows carry "_sample": true and diagnostic rows carry "_diagnostic": true with an errorCode you can filter on, and neither is ever billed.
How it works
- Each retailer publishes one JSON file of its own forecourt prices at its own address. The Actor reads the files for the retailers you asked for, straight over HTTP. There is nothing to log into and no page to render.
- Every file is checked before anything is billed: that it parsed, that it contains a station list, that the list is not empty, that it carries a timestamp, and that the timestamp is recent enough for what you asked. A file that fails any of those becomes a free row explaining which check it failed.
- Grades are mapped onto one set of field names, prices are read as pence per litre, and two placeholder values retailers use to mean "we do not sell this grade here" are turned into nulls rather than shipped as prices.
- A retailer that has taken its file offline is asked once, not repeatedly. There is nothing to be gained from asking a 404 a second time and you would wait for it.
What people use it for
- Watching what one chain charges across the country, and where it is cheapest. One run, filter the rows, sort on the grade you buy.
- Costing a fleet or a delivery round: pull the stations on the route by postcode area and compare diesel against what you are paying on account.
- Feeding a price-comparison page or an in-car app with a flat, typed table instead of scraping fifteen separate websites.
- Building a price history nobody else has. The published files are a snapshot, overwritten when a price changes and no archive exists, so the only way to get yesterday is to have saved it. Run this on a schedule and you are the archive.
- Checking a specific forecourt before a long drive, using the postcode filter rather than pulling the country.
Which retailers you actually get
This is the part to read first, because it decides what the data is worth to you.
UK fuel retailers began publishing these files under a voluntary arrangement, and the government listed the addresses on one page. That page was withdrawn on 1 May 2026 when a statutory scheme replaced it. The statutory scheme is not an open file. It is an API behind a registration and a credential, so what this Actor reads is the retailers' own published files, which most of them have carried on serving.
Most, not all. Measured on 20 September 2026:
| Retailer | Key | Stations | State |
|---|---|---|---|
| Motor Fuel Group | mfg | 1,224 | refreshed that morning |
| Asda | asda | 790 | refreshed the previous day |
| Esso Tesco Alliance | esso | 193 | refreshed that morning |
| SGN | sgn | 112 | refreshed the previous day |
| Moto | moto | 47 | refreshed that morning |
| JET Retail UK | jet | 11 | refreshed the previous day |
| Morrisons | morrisons | 1 | refreshed, but see below |
| Shell | shell | 546 | file frozen since 13 May 2026 |
| Tesco | tesco | 515 | file frozen since 29 April 2026 |
| Rontec | rontec | 265 | file frozen since 1 May 2026 |
| Applegreen | applegreen | 65 | file frozen since 26 February 2025 |
| Ascona Group | ascona | 60 | file frozen since 5 May 2026 |
| bp | bp | none | refuses the request |
| Karan Retail | karan | none | gateway error |
| Sainsbury's | sainsburys | none | address no longer resolves |
So on the default settings you get roughly 2,400 stations with current prices, from seven retailers. The five frozen files are skipped and reported rather than charged, because a price from April is not a price. If you want them anyway, for coverage of a site rather than for its price, raise maxFeedAgeHours and they arrive with feedStale: true and their real age on every row.
That table is what was true on the day this was written. Nothing here is hardcoded around it: all fifteen retailers are asked every run, so a file that comes back to life simply starts returning rows, and one that stops starts returning a free diagnostic row instead. Check feedUpdatedAt on your own run rather than trusting the table.
A few retailers, and SGN, Ascona and Tesco have all done it, refuse a second request that arrives too soon after the first and then hand the file over normally a moment later. The run waits and asks again rather than giving up, so you will not usually notice. If you are running this several times in parallel you might, and the retailer that missed out says so in its own free row.
Morrisons deserves its own line. Its file is refreshed daily and contains exactly one station, in Gibraltar. That is what they publish; it is not a bug at this end, and it is why morrisons is in the list but worth almost nothing.
Reading the output
Every run writes three kinds of row and they are easy to tell apart.
- Real rows carry
"charged": trueand"recordType": "station". One billed event each. - The sample row carries
"_sample": trueand"charged": false. There is exactly one, it only appears when you ran with no retailers chosen, and it is there so you can see the shape before spending anything. - Diagnostic rows carry
"_diagnostic": true,"charged": false, theretailerthey belong to and anerrorCodeyou can switch on:FEED_STALEwhen a retailer's file was older than your limit,FEED_HTTPwhen it answered with an error status,FEED_UNREACHABLEwhen it could not be reached at all,FEED_NOT_JSONwhen it answered with something that was not a price list,FEED_EMPTYwhen it published no stations,FEED_NO_TIMESTAMPorFEED_BAD_TIMESTAMPwhen there is no readable date on it,NO_RESULTSwhen nothing it published matched your filter.
If you only want data, filter on charged == true. The count of those rows is exactly the number of events you were billed for, so the dataset is its own invoice.
Why the timestamp matters more than usual here
A stale scrape of most things is obvious. The row is missing, or the number is absent, or the page 404s. A stale fuel price is none of those. It is a plausible number in the right field on a file that answers HTTP 200, and there is nothing in the response that says "this is five months old" except the timestamp.
That is the whole reason feedUpdatedAt and feedAgeHours are on every row and the age limit is checked before anything is charged. It is also why raising maxFeedAgeHours is a deliberate act rather than a default: you can have the frozen files, but you have to ask.
Limitations
- This is not every filling station in the UK. It is the stations belonging to retailers who publish an open file, which is roughly 2,400 with current prices against something over 8,000 forecourts nationally. Independents and several large chains are not in it.
- bp, Karan Retail and Sainsbury's do not answer at all today. Asking for them costs you nothing and returns a row saying what happened.
- Shell, Tesco, Rontec, Ascona and Applegreen still serve a file, but none has been refreshed since spring 2026 or earlier. Under the default settings you will get no stations from them, only a free row explaining why.
- Prices are a snapshot of whatever the retailer last published, not a live read of the pump. A forecourt that changed its price an hour ago may not have refreshed its file yet.
- There is no price history. The published files are overwritten in place, so there is no way to ask for last week. If you want history you have to collect it yourself, run by run.
- Timestamps are published without a time zone and are read as UTC. In British Summer Time that can make a file look an hour older than it is, which matters to nobody using the 48-hour default and might matter if you set the limit very low.
- Not every site sells every grade. Super unleaded appears on about seven sites in ten and premium diesel on fewer; those fields are null rather than zero.
- Two placeholder values retailers use for "we do not sell this here" are treated as absent rather than as prices. If a real price ever falls below 50p or rises above 400p a litre it would be discarded the same way, which is a trade this Actor makes on purpose.
- Four sites publish no usable coordinates and arrive with null latitude and longitude. Their address and postcode are correct.
- Morrisons publishes exactly one station, in Gibraltar. It is included because it is what they publish, not because it is useful.
- The postcode filter matches on the start of the string, so
M1also matchesM11andM12. Use the full outward code if that matters. - The hard ceiling is 20,000 rows per run, which is far above everything currently published.
Questions
How current are the prices?
Every row tells you, in feedUpdatedAt and feedAgeHours. Under the default settings nothing older than 48 hours is charged, and in practice the seven live retailers refresh daily, so rows are usually a few hours old. Retailers whose files have stopped moving are skipped and reported rather than being mixed in.
What happens if a retailer's feed is down?
You get one free diagnostic row for that retailer naming it and saying what went wrong: the HTTP status, or that the address did not resolve. The run carries on to the others, and you are never billed for a retailer that returned nothing.
Does it cover every petrol station in the UK?
No, and that is worth being clear about. It covers the retailers who publish an open file, which is around 2,400 stations with current prices. Independents and some large chains do not publish one, so they are not here.
Can I get the price at one specific station?
Use postcodePrefixes with the full outward code and pick the row you want. You are charged only for the stations that match; everything the filter drops is free.
Can I get historical prices?
Not from this, because they do not exist to be got. Retailers overwrite the file when a price changes and keep no archive. Schedule this daily and you build the history yourself.
Why do some rows have null where a price should be?
Either the site does not sell that grade, or the retailer published one of the placeholder values that mean the same thing. Both come through as null so you can tell "not sold" from a real number without checking for magic values yourself.
Do I need a proxy or an API key?
Neither. The files are published openly and the run brings its own egress. The proxyUrls field is only there for callers who want traffic to leave through servers they already own.
Will the run fail if something goes wrong?
No. A missing, broken or stale file produces an uncharged row explaining what happened and the run still finishes as succeeded. A failed run would still bill the start fee, which would mean paying to be told something went wrong.
Can I run it on a schedule?
Yes, and it is the obvious way to use it. Nothing is held between runs. Key on retailerKey plus siteId plus postcode to match a station across runs, and watch feedUpdatedAt to see whether a retailer actually moved since last time.