FuelHunter
UK avg E10 166.3p B7S 187.7p

Fuel Finder API: how to actually use it

Working notes on the UK government's Fuel Finder service. Some of this is in the official docs. The rest we could not find documented anywhere, so we worked it out by calling the API.

Fuel Finder publishes near live pump prices for all 8,000 or so UK forecourts. We pull it twice a day for FuelHunter, so these notes come from running against the API rather than from reading the portal.

API last checked 3 September 2026.

You need a UK IP address

The API only answers requests from a UK address. From anywhere else it returns 403 before it looks at your credentials, so a valid token makes no difference.

The 403 carries server: CloudFront, x-cache: Error from cloudfront and content-length: 0. There is no JSON error and no message in the body, which makes it easy to mistake for a network problem at your end.

We tested from a UK connection and from two cloud data centres, on the same credentials with plain curl:

Request fromRootAuthenticated pull
UK301Full pull, 500 records a batch
AWS, US403, empty bodyNot reached
Hetzner, Germany403, empty bodyNot reached

It applies to the whole host rather than to particular routes. /, /api/v1/pfs, /robots.txt and a path that does not exist all return the same 403. TLS completes normally and you get a full HTTP/2 response back, so there is nothing to fix in your client. Changing the user agent does not help.

If you are planning to run this from a cloud region outside the UK, plan for it now. Either host in the UK, or fetch in the UK and move the result to wherever the rest of your stack lives.

Getting a token

OAuth 2.0, client credentials. Tokens last an hour. Credentials come from the developer portal, signing in with GOV.UK One Login.

The base URL is:

https://www.fuel-finder.service.gov.uk/api/v1

Worth setting that against the portal's worked examples, which use api.fuelfinder.service.gov.uk with no hyphen. That host does not resolve, so use the one above.

POST https://www.fuel-finder.service.gov.uk/api/v1/oauth/generate_access_token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
client_id=<id>
client_secret=<secret>
scope=fuelfinder.read

The token comes back nested under data, at data.access_token, so reading access_token from the top of the response gives you nothing and every later call returns 401. Send it as Authorization: Bearer <token>.

Endpoints and paging

EndpointReturns
GET /pfs?batch-number=NForecourt info: name, brand, location, amenities, opening times
GET /pfs/fuel-prices?batch-number=NPrices per grade, with submission and effective timestamps

Both page at 500 records, starting at batch-number=1. The UK needs 17 batches per endpoint at the moment.

Paging ends with a 404. An empty 200 is not the end of the data, it is a blip, and treating it as the end will silently cut your pull short. That has happened to us twice. Once prices stopped at batch 11 and we shipped 5,000 of about 7,980 records, which looked like a lot of stations simply having no price. Once batch 1 came back empty, an all blank file passed a row count check, and the site showed no prices at all.

So retry an empty page the way you would retry a 500, and check coverage before you let a pull replace data you already have. Sporadic 500, 503 and 504 responses and dropped connections turn up mid run as well, historically in something like one run out of three. Back off and retry.

Rate limits

LimitValue
Requests per minute, per client100, live and test alike
Concurrent requests1
Over the limit429

The portal also suggests caching station data for an hour, prices for 15 minutes and search results for 5 minutes.

A full pull is 35 requests, 17 batches from each endpoint plus the token, and takes about three and a half minutes single threaded with a half second gap between batches. That works out at roughly 10 requests a minute against a limit of 100. Polling every 30 minutes, which matches how often the service publishes, uses about 12% of the allowance.

The API publishes within 30 minutes of a change. The twice daily figure you may have seen applies to the manual CSV download from the portal.

We have not found a delta or changed-since parameter. If one exists, a poll drops from 35 requests to a handful.

The CSV columns

68 columns, in six groups:

GroupColumnsPrefix
Forecourt10forecourts.
Location8forecourts.location.
Prices18forecourts.fuel_price. plus two timestamp families
Opening hours21forecourts.opening_times.usual_days.<day>.
Bank holiday3forecourts.opening_times.bank_holiday.standard.
Amenities8forecourts.amenities.

Six fuel grades, E5, E10, B7S (standard diesel), B7P (premium diesel), B10 and HVO. Each has three columns:

forecourts.fuel_price.<GRADE>
forecourts.price_submission_timestamp.<GRADE>
forecourts.price_change_effective_timestamp.<GRADE>

Opening hours are seven blocks of three, with the day in the column name rather than in the row order:

forecourts.opening_times.usual_days.monday.open_time
forecourts.opening_times.usual_days.monday.close_time
forecourts.opening_times.usual_days.monday.is_24_hours

The full list, with a type and group against each column, is in the companion repository, along with a short Python client that handles the token nesting, the 404 and the empty page retry.

Where the CSV guide differs from the file

The CSV guide lists the columns in a shortened display form. The header in the file uses the full nested path, so the names in the guide will not match what you read:

Shown in the guideIn the file
forecourts.postcodeforecourts.location.postcode
forecourts.latitudeforecourts.location.latitude
forecourts.car_washforecourts.amenities.vehicle_services.car_wash

The whole of the location, amenities and opening hours blocks are affected. Match on the short names and your parser still runs. It picks up the station level fields and returns nothing for the rest, so the data looks absent rather than unmatched. Worth checking first if you are seeing empty columns.

The portal's REST API examples have the same problem with the host name, covered above.

Reading the values

Prices arrive in mixed units. Most retailers submit pence per litre, so 138.9, and some submit pounds per litre, so 1.389. Nothing in the row tells you which. Treating anything under 10 as pounds and multiplying by 100 has worked on every file we have seen.

There are two timestamp formats. Price and forecourt update timestamps come in a JavaScript style:

Sat Mar 07 2026 15:34:42 GMT+0000 (Coordinated Universal Time)

Closure dates come in something closer to ISO:

2025-09-05 14:30:00

Parse both, and keep the raw string when neither matches.

The price submission timestamp records the last time the price changed, not the last time it was confirmed. Retailers are told to submit changes only, and not to resend an unchanged price with a new timestamp. Our own history bears that out: of 438,254 consecutive price pairs, 1.8% repeat the previous value. A station showing a two week old timestamp has usually held its price for two weeks, so anything you build on that field measures price stability rather than data freshness.

Two fields worth not trusting. is_supermarket_service_station disagrees with reality often enough that deriving supermarket status from brand_name against a known list is more accurate. country is not written consistently, so map it against known values rather than reading it straight.

Booleans are the uppercase strings TRUE and FALSE. Missing values are empty cells throughout, so parse leniently.

Attribution

The data is Crown copyright under the Open Government Licence v3.0. The developer guidelines ask recipients not to pass the raw feed on, so there is no price data on this page or in the repository, only the schema and the notes. Our data sources page covers how FuelHunter uses it.