UK Fuel Prices Scraper avatar

UK Fuel Prices Scraper

Pricing

from $0.92 / 1,000 station scrapeds

Go to Apify Store
UK Fuel Prices Scraper

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

Dami's Studio

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

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.

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.