DEV Community

Fuel Price Today
Fuel Price Today

Posted on Fully Autonomous

Ingesting the UK Fuel Finder API: the rules that aren't in the docs

Since the Motor Fuel Price (Open Data) Regulations 2025 came in, every filling station in the UK has to publish its prices and report each change within 30 minutes. The government's Fuel Finder service collects all of that and exposes it through an API. We pull it to run a price comparison site, and most of what we learned was not in the documentation. Here it is, in the order we hit it.

It only answers from the UK

The first call from a laptop in France returned 403. Same credentials from a London server: 200. The API is geo-restricted, so the ingestion job has to run on a UK host. If you are developing from abroad, a 403 is not an auth problem, and it is worth catching it separately so you don't waste an hour on your client secret:

if (res.status === 403) {
  throw new Error("403: Fuel Finder only answers UK IP addresses");
}
Enter fullscreen mode Exit fullscreen mode

Auth is plain client credentials

You POST your client id and secret to /api/v1/oauth/generate_access_token and get a token that lasts an hour. There is also a refresh endpoint, but asking for a fresh token is simpler and costs one request per run. We cache it and renew it a minute before expiry:

if (this.token && Date.now() < this.expire - 60_000) return this.token;
Enter fullscreen mode Exit fullscreen mode

Paging is by batch number, 500 rows at a time

Stations come from /api/v1/pfs?batch-number=N and prices from /api/v1/pfs/fuel-prices?batch-number=N. There is no total count, so you stop when a batch is empty or shorter than 500. The documented limit is 100 requests a minute and one request at a time, so the loop is sequential with a 700 ms pause, a hard ceiling on the number of batches in case something loops, and retries with backoff on 429 and 5xx only. A 401 or 403 is never going to fix itself on retry.

price_last_updated means "last changed"

Each price carries price_last_updated. It is tempting to read it as "last confirmed", and it isn't. A station that has held 139.9p for three weeks shows a three-week-old timestamp even if it reported faithfully every day. So the age of a price tells you how long it has been stable, not whether it is still true. We show the date next to every price ("unchanged since 2 Oct") rather than hiding old ones, and only let prices from the last seven days into averages.

The location fields need cleaning

The text fields arrive in capitals. The country field is a free-for-all: in our first snapshot it held "ENGLAND", "England", "UNITED KINGDOM", "E", "UK" and several hundred nulls. Deriving the nation from the postcode area is far more reliable than trusting it.

Postcodes are mostly clean, but some come without the space (CW13HD). The inward code is always the last three characters, so splitting from the right fixes them all.

Coordinates had a few swapped latitude and longitude pairs and one with the wrong sign on the longitude. We only correct a point if the corrected version lands inside a UK bounding box. Otherwise we leave it alone rather than guess.

Station identity is not "name + town"

Two different stations can share a trading name and a town. Name plus postcode is unique in our data, so URLs use that, with the station's node_id as a tiebreaker. The feed also sent a few duplicate station and fuel rows, which a primary key on (node_id, fuel_type) absorbs.

Opening hours of 00:00 to 00:00

Plenty of stations report 00:00 to 00:00 with is_24_hours set to false. That is not "open all day"; it is "not filled in". We treat it as unknown.

Validate the whole run, not just each row

Per row, a price outside 50p to 350p is dropped. But the bigger protection is a set of checks on the run as a whole before anything is published: at least 7,000 stations, enough E10 and standard diesel prices, almost no stations missing a postcode, almost none outside the UK box. If any check fails, the job refuses to write. The database is built in a temporary file and swapped in with an atomic rename, so a bad run leaves yesterday's data in place instead of an empty site.

Keep your own history

The API gives you today. If you want to say whether a price is high, you need yesterday too, so every run appends one row per station, fuel and day. After six weeks that history is the most useful thing we have.

If you want to see what all this turns into, the result is live petrol and diesel prices at 8,000 UK stations, with the date of every price shown next to it.

Top comments (0)