Request a tool
All toolsAutomationsGuidesMCP serverRequest a toolPlatformsCategories
UK Fuel Prices Scraper icon

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.

19 runs on Apify $0.00092 per station ($0.92 / 1,000)
Run this in the cloudRun on Apify →

Developer & Research Tools

How it works

  1. 1
    Open it on Apify

    Hit Run on Apify — it opens the tool in the cloud, no install.

  2. 2
    Set the inputs

    Adjust retailers, maxItems, maxFeedAgeHours (sensible defaults are pre-filled).

  3. 3
    Click Run

    The tool runs on Apify’s cloud and collects the data for you.

  4. 4
    Export 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 forWhenPrice
Station scrapedOne 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

FieldWhat it doesType
retailersWhich 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
maxItemsTotal 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
maxFeedAgeHoursHow 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
postcodePrefixesLeave 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
proxyUrlsLeave 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:

retailerbrandsiteIdaddresspostcodelatitudelongitudepetrolE10petrolE5dieselB7dieselSDVfeedUpdatedAtfeedAgeHours

Export 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 iconDeveloper & Research Tools

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.

18 use cases

Stack Overflow / Stack Exchange Scraper iconDeveloper & Research Tools

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.

2 use cases

Package Registry Scraper (npm + PyPI) iconDeveloper & Research Tools

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.

2 use cases

arXiv Scraper iconDeveloper & Research Tools

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.

2 use cases

OpenAlex Scholarly Works Scraper iconDeveloper & Research Tools

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.

2 use cases

Crossref Scholarly Works Scraper iconDeveloper & Research Tools

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.

2 use cases

See all 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.

StationsTotal 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-scraped event 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
}
FieldWhat it does
retailersWhich 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.
maxItemsTotal 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.
maxFeedAgeHoursHow 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.
postcodePrefixesOptional. ["SW","M1","EH12"] returns only stations whose postcode starts with one of those. Stations the filter drops are never charged.
proxyUrlsLeave 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 both SHELL and Shell.
  • 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 than 0, 0 when 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 raised maxFeedAgeHours to 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:

RetailerKeyStationsState
Motor Fuel Groupmfg1,224refreshed that morning
Asdaasda790refreshed the previous day
Esso Tesco Allianceesso193refreshed that morning
SGNsgn112refreshed the previous day
Motomoto47refreshed that morning
JET Retail UKjet11refreshed the previous day
Morrisonsmorrisons1refreshed, but see below
Shellshell546file frozen since 13 May 2026
Tescotesco515file frozen since 29 April 2026
Rontecrontec265file frozen since 1 May 2026
Applegreenapplegreen65file frozen since 26 February 2025
Ascona Groupascona60file frozen since 5 May 2026
bpbpnonerefuses the request
Karan Retailkarannonegateway error
Sainsbury'ssainsburysnoneaddress 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": true and "recordType": "station". One billed event each.
  • The sample row carries "_sample": true and "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, the retailer they belong to and an errorCode you can switch on: FEED_STALE when a retailer's file was older than your limit, FEED_HTTP when it answered with an error status, FEED_UNREACHABLE when it could not be reached at all, FEED_NOT_JSON when it answered with something that was not a price list, FEED_EMPTY when it published no stations, FEED_NO_TIMESTAMP or FEED_BAD_TIMESTAMP when there is no readable date on it, NO_RESULTS when 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 M1 also matches M11 and M12. 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.