UK Fuel Prices Scraper
Scrape live UK petrol and diesel prices per filling station: brand, postcode, coordinates and every grade, from the retailers' own open data feeds.
How it works
- 1Open it on Apify
Hit Run on Apify — it opens the tool in the cloud, no install.
- 2Set the inputs
Adjust
retailers,maxItems,maxFeedAgeHours(sensible defaults are pre-filled). - 3Click Run
The tool runs on Apify’s cloud and collects the data for you.
- 4Export the results
Download as JSON, CSV or Excel, or pipe straight into your app, Google Sheets, or an AI agent.
Pricing
$0.00092 per station = $0.92 per 1,000
| You are charged for | When | Price |
|---|---|---|
| Station scraped | One filling station with its current pump prices. Stale or unreachable retailer feeds are never charged. | $0.00092 |
Pay-per-event pricing: you are billed per result, not per subscription. Billing is handled by Apify on your own account. These are the live Apify store prices, in effect since 2026-09-20, and they are what you are actually charged.
Inputs
| Field | What it does | Type |
|---|---|---|
retailers | Which retailers to read. Use the single word all for every one of them. Currently publishing and up to date: asda, esso, jet, mfg, morrisons, moto, sgn. Still answering but no longer refreshed: ascona, applegreen, rontec, shell, tesco - these are skipped unless you raise the feed age limit below. Not answering at all right now: bp, karan, sainsburys. Ask for one of those and you get a free row telling you so. | array |
maxItems | Total stations to return across all the retailers you chose. Retailers are read in the order you list them, so put the one you care most about first; with all, they are read biggest first. The whole set across every live retailer is roughly 2,400 stations. Keep this low while you are testing - you pay per station. | integer |
maxFeedAgeHours | How old a retailer's own file is allowed to be before its stations are skipped. Several retailers still serve a file that has not been refreshed since April, and those prices are not what a driver pays today. Anything older than this is reported as a free row and never charged. The default of 48 hours keeps only genuinely current data. Raise it if you want the frozen files as well - every row tells you how old it is either way. | integer |
postcodePrefixes | Leave empty for everything. Otherwise list the start of the postcodes you want, such as SW, M1 or EH12, and only stations whose postcode begins with one of them are returned. Stations dropped by this filter are never charged. | array |
proxyUrls | Leave this empty for a normal run. Fill it in only if you want the traffic to leave through proxy servers you already pay for, one URL per line, in the form http://user:pass@host:port. | array |
What you get
A structured dataset — each result includes fields like:
retailerbrandsiteIdaddresspostcodelatitudelongitudepetrolE10petrolE5dieselB7dieselSDVfeedUpdatedAtfeedAgeHoursExport every run as JSON, CSV or Excel, or send it to your app, a database, Google Sheets, or an AI agent.
Related tools in Developer & Research Tools
Other ready-to-run tools in the same category — all pay-per-use on the Apify cloud.
GitHub Scraper
Search GitHub repos and users: stars, forks, language, topics, licence, plus user bio, company and followers. No token needed. $0.90 per 1,000 rows.
Stack Overflow / Stack Exchange Scraper
Search Stack Overflow and Stack Exchange by keyword or tag. Score, answer count, views, reputation and body text. $2 per 1,000 questions.
Package Registry Scraper (npm + PyPI)
Get npm and PyPI package metadata as JSON. Version, license, author, repo, keywords and npm monthly downloads. $2 per 1,000 packages.
arXiv Scraper
Search arXiv papers by title, author, abstract or category. Get full abstracts, authors, categories, DOI, dates and PDF links. $2 per 1,000 papers.
OpenAlex Scholarly Works Scraper
Search 250M+ OpenAlex papers with no API key. Get titles, authors, venue, year, citations, DOI, OA links and full abstracts. $2.00 per 1,000 papers.
Crossref Scholarly Works Scraper
Search 150M+ papers on Crossref: DOI, title, authors, journal, publisher, date, citations and abstract. No API key. $1.00 per 1,000 works.
Where this tool sits
- Categories
- Developer & Research Tools
UK Fuel Prices Scraper
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.